Spring Cloud 参考文档(Spring Cloud Commons:通用的抽象)

Spring Cloud Commons:通用的抽象服务发现、负载均衡和断路器等模式适用于所有Spring Cloud客户端都可以使用的通用抽象层,独立于实现(例如,使用Eureka或Consul发现)。@EnableDiscoveryClientSpring Cloud Commons提供@EnableDiscoveryClient注解,这将使用META-INF/spring.factories查找DiscoveryClient接口的实现。Discovery Client的实现将配置类添加到org.springframework.cloud.client.discovery.EnableDiscoveryClient键下的spring.factories,DiscoveryClient实现的示例包括Spring Cloud Netflix Eureka,Spring Cloud Consul Discovery和Spring Cloud Zookeeper Discovery。默认情况下,DiscoveryClient的实现会使用远程发现服务器自动注册本地Spring Boot服务器,通过在@EnableDiscoveryClient中设置autoRegister=false可以禁用此行为。@EnableDiscoveryClient已不再需要,你可以在类路径上放置DiscoveryClient实现,以使Spring Boot应用程序向服务发现服务器注册。健康指示器Commons创建了一个Spring Boot HealthIndicator,DiscoveryClient实现可以通过实现DiscoveryHealthIndicator来参与,要禁用混合HealthIndicator,请设置spring.cloud.discovery.client.composite-indicator.enabled=false。基于DiscoveryClient的通用HealthIndicator是自动配置的(DiscoveryClientHealthIndicator)。要禁用它,请设置spring.cloud.discovery.client.health-indicator.enabled=false,要禁用DiscoveryClientHealthIndicator的description字段,请设置spring.cloud.discovery.client.health-indicator.include-description=false,否则,它可能会像卷起的HealthIndicator的description一样冒出来。排序DiscoveryClient实例DiscoveryClient接口扩展了Ordered,这在使用多个发现客户端时很有用,因为它允许你定义返回的发现客户端的顺序,类似于你可以如何排序Spring应用程序加载的bean。默认情况下,任何DiscoveryClient的顺序都设置为0,如果要为自定义DiscoveryClient实现设置不同的顺序,只需重写getOrder()方法,以便它返回适合你的设置的值。除此之外,你还可以使用属性来设置Spring Cloud提供的DiscoveryClient实现的顺序,其中包括ConsulDiscoveryClient,EurekaDiscoveryClient和ZookeeperDiscoveryClient,为此,你只需将spring.cloud.{clientIdentifier}.discovery.order(或Eureka的eureka.client.order)属性设置为所需的值。ServiceRegistryCommons现在提供一个ServiceRegistry接口,提供register(Registration)和deregister(Registration)等方法,让你提供自定义注册服务,Registration是一个标记接口。以下示例显示ServiceRegistry的使用:@Configuration@EnableDiscoveryClient(autoRegister=false)public class MyConfiguration { private ServiceRegistry registry; public MyConfiguration(ServiceRegistry registry) { this.registry = registry; } // called through some external process, such as an event or a custom actuator endpoint public void register() { Registration registration = constructRegistration(); this.registry.register(registration); }}每个ServiceRegistry实现都有自己的Registry实现。ZookeeperRegistration与ZookeeperServiceRegistry一起使用EurekaRegistration与EurekaServiceRegistry一起使用ConsulRegistration与ConsulServiceRegistry一起使用如果你使用的是ServiceRegistry接口,则需要为正在使用的ServiceRegistry实现传递正确的Registry实现。ServiceRegistry自动注册默认情况下,ServiceRegistry实现会自动注册正在运行的服务,要禁用该行为,你可以设置: @EnableDiscoveryClient(autoRegister=false)永久禁用自动注册, spring.cloud.service-registry.auto-registration.enabled=false通过配置禁用行为。ServiceRegistry自动注册事件当服务自动注册时,将触发两个事件,第一个事件名为InstancePreRegisteredEvent,在注册服务之前触发,第二个事件名为InstanceRegisteredEvent,在注册服务后触发,你可以注册一个ApplicationListener来监听并响应这些事件。如果spring.cloud.service-registry.auto-registration.enabled设置为false,则不会触发这些事件。Service Registry Actuator端点Spring Cloud Commons提供/service-registry执行器端点,此端点依赖于Spring Application Context中的Registration bean,使用GET调用/service-registry返回Registration的状态,将POST用于具有JSON体的同一端点会将当前Registration的状态更改为新值,JSON体必须包含具有首选值的status字段。在更新状态和为状态返回的值时,请参阅用于允许值的ServiceRegistry实现的文档,例如,Eureka支持的状态是UP、DOWN、OUT_OF_SERVICE和UNKNOWN。Spring RestTemplate作为负载均衡客户端RestTemplate可以自动配置为使用ribbon,要创建负载均衡的RestTemplate,请创建RestTemplate @Bean并使用@LoadBalanced限定符,如以下示例所示:@Configurationpublic class MyConfiguration { @LoadBalanced @Bean RestTemplate restTemplate() { return new RestTemplate(); }}public class MyClass { @Autowired private RestTemplate restTemplate; public String doOtherStuff() { String results = restTemplate.getForObject(“http://stores/stores”, String.class); return results; }}不再通过自动配置创建RestTemplate bean,单个应用程序必须创建它。URI需要使用虚拟主机名(即服务名称,而不是主机名),Ribbon客户端用于创建完整的物理地址,有关如何设置RestTemplate的详细信息,请参见RibbonAutoConfiguration。Spring WebClient作为负载均衡客户端WebClient可以自动配置为使用LoadBalancerClient,要创建负载均衡的WebClient,请创建WebClient.Builder @Bean并使用@LoadBalanced限定符,如以下示例所示:@Configurationpublic class MyConfiguration { @Bean @LoadBalanced public WebClient.Builder loadBalancedWebClientBuilder() { return WebClient.builder(); }}public class MyClass { @Autowired private WebClient.Builder webClientBuilder; public Mono<String> doOtherStuff() { return webClientBuilder.build().get().uri(“http://stores/stores”) .retrieve().bodyToMono(String.class); }}URI需要使用虚拟主机名(即服务名称,而不是主机名),Ribbon客户端用于创建完整的物理地址。重试失败的请求可以将负载均衡的RestTemplate配置为重试失败的请求,默认情况下,禁用此逻辑,你可以通过将Spring Retry添加到应用程序的类路径来启用它。负载均衡的RestTemplate支持与重试失败的请求相关的一些Ribbon配置值,你可以使用client.ribbon.MaxAutoRetries、client.ribbon.MaxAutoRetriesNextServer和client.ribbon.OkToRetryOnAllOperations属性,如果要在类路径上使用Spring Retry禁用重试逻辑,可以设置spring.cloud.loadbalancer.retry.enabled=false,有关这些属性的说明,请参阅Ribbon文档。如果要在重试中实现BackOffPolicy,则需要创建LoadBalancedRetryFactory类型的bean并覆盖createBackOffPolicy方法:@Configurationpublic class MyConfiguration { @Bean LoadBalancedRetryFactory retryFactory() { return new LoadBalancedRetryFactory() { @Override public BackOffPolicy createBackOffPolicy(String service) { return new ExponentialBackOffPolicy(); } }; }}前面示例中的client应替换为你的Ribbon客户端的名称。如果要将一个或多个RetryListener实现添加到重试功能中,你需要创建一个类型为LoadBalancedRetryListenerFactory的bean并返回你要用于给定服务的RetryListener数组,如以下示例所示:@Configurationpublic class MyConfiguration { @Bean LoadBalancedRetryListenerFactory retryListenerFactory() { return new LoadBalancedRetryListenerFactory() { @Override public RetryListener[] createRetryListeners(String service) { return new RetryListener[]{new RetryListener() { @Override public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) { //TODO Do you business… return true; } @Override public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { //TODO Do you business… } @Override public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { //TODO Do you business… } }}; } }; }}多个RestTemplate对象如果你想要一个非负载均衡的RestTemplate,请创建一个RestTemplate bean并将其注入,要访问负载均衡的RestTemplate,请在创建@Bean时使用@LoadBalanced限定符,如以下示例所示:@Configurationpublic class MyConfiguration { @LoadBalanced @Bean RestTemplate loadBalanced() { return new RestTemplate(); } @Primary @Bean RestTemplate restTemplate() { return new RestTemplate(); }}public class MyClass { @Autowired private RestTemplate restTemplate; @Autowired @LoadBalanced private RestTemplate loadBalanced; public String doOtherStuff() { return loadBalanced.getForObject(“http://stores/stores”, String.class); } public String doStuff() { return restTemplate.getForObject(“http://example.com”, String.class); }}请注意在前面示例中的普通RestTemplate声明中使用@Primary注解来消除无条件的@Autowired注入的歧义。如果你看到java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89,尝试注入RestOperations或设置spring.aop.proxyTargetClass=true。Spring WebFlux WebClient作为负载均衡客户端可以将WebClient配置为使用LoadBalancerClient,如果spring-webflux位于类路径上,则自动配置LoadBalancerExchangeFilterFunction,以下示例显示如何配置WebClient以使用负载均衡:public class MyClass { @Autowired private LoadBalancerExchangeFilterFunction lbFunction; public Mono<String> doOtherStuff() { return WebClient.builder().baseUrl(“http://stores”) .filter(lbFunction) .build() .get() .uri("/stores") .retrieve() .bodyToMono(String.class); }}URI需要使用虚拟主机名(即服务名称,而不是主机名),LoadBalancerClient用于创建完整的物理地址。忽略网络接口有时,忽略某些命名的网络接口以便从Service Discovery注册中排除它们(例如,在Docker容器中运行时)是有用的,可以设置正则表达式列表以使所需的网络接口被忽略,以下配置忽略docker0接口和以veth开头的所有接口:application.ymlspring: cloud: inetutils: ignoredInterfaces: - docker0 - veth.你还可以使用正则表达式列表强制仅使用指定的网络地址,如以下示例所示:bootstrap.ymlspring: cloud: inetutils: preferredNetworks: - 192.168 - 10.0你还可以强制仅使用站点本地地址,如以下示例所示:application.ymlspring: cloud: inetutils: useOnlySiteLocalInterfaces: true有关构成站点本地地址的更多详细信息,请参阅Inet4Address.html.isSiteLocalAddress()。HTTP客户端工厂Spring Cloud Commons提供用于创建Apache HTTP客户端(ApacheHttpClientFactory)和OK HTTP客户端(OkHttpClientFactory)的bean,仅当OK HTTP jar位于类路径上时,才会创建OkHttpClientFactory bean。此外,Spring Cloud Commons提供了创建用于两个客户端使用的连接管理器的bean:Apache HTTP客户端的ApacheHttpClientConnectionManagerFactory和OK HTTP客户端的OkHttpClientConnectionPoolFactory。如果要自定义在下游项目中创建HTTP客户端的方式,可以提供自己的这些bean实现,此外,如果你提供类型为HttpClientBuilder或OkHttpClient.Builder的bean,则默认工厂使用这些构建器作为返回到下游项目的构建器的基础,你还可以通过将spring.cloud.httpclientfactories.apache.enabled或spring.cloud.httpclientfactories.ok.enabled设置为false来禁用这些bean的创建。启用功能Spring Cloud Commons提供/features执行器端点,此端点返回类路径上可用的功能以及它们是否已启用,返回的信息包括功能类型、名称、版本和供应商。功能类型有两种类型的’功能’:抽象和命名。抽象功能是定义接口或抽象类并创建实现(如DiscoveryClient、LoadBalancerClient或LockService)的功能,抽象类或接口用于在上下文中查找该类型的bean,显示的版本是bean.getClass().getPackage().getImplementationVersion()。命名功能是没有他们实现的特定类的功能,例如“断路器”,“API网关”,“Spring Cloud Bus”等,这些功能需要名称和bean类型。声明功能任何模块都可以声明任意数量的HasFeature bean,如以下示例所示:@Beanpublic HasFeatures commonsFeatures() { return HasFeatures.abstractFeatures(DiscoveryClient.class, LoadBalancerClient.class);}@Beanpublic HasFeatures consulFeatures() { return HasFeatures.namedFeatures( new NamedFeature(“Spring Cloud Bus”, ConsulBusAutoConfiguration.class), new NamedFeature(“Circuit Breaker”, HystrixCommandAspect.class));}@BeanHasFeatures localFeatures() { return HasFeatures.builder() .abstractFeature(Foo.class) .namedFeature(new NamedFeature(“Bar Feature”, Bar.class)) .abstractFeature(Baz.class) .build();}这些bean中的每一个都应该放在一个受到适当保护的@Configuration中。Spring Cloud兼容性验证由于某些用户在设置Spring Cloud应用程序时遇到问题,因此决定添加兼容性验证机制,如果你当前的设置与Spring Cloud要求不兼容,并且报告显示出现了什么问题,它将会中断。目前我们验证哪个版本的Spring Boot被添加到你的类路径中。报告示例APPLICATION FAILED TO START*Description:Your project setup is incompatible with our requirements due to following reasons:- Spring Boot [2.1.0.RELEASE] is not compatible with this Spring Cloud release trainAction:Consider applying the following actions:- Change Spring Boot version to one of the following versions [1.2.x, 1.3.x] .You can find the latest Spring Boot versions here [https://spring.io/projects/spring-boot#learn].If you want to learn more about the Spring Cloud Release train compatibility, you can visit this page [https://spring.io/projects/spring-cloud#overview] and check the [Release Trains] section.要禁用此功能,请将spring.cloud.compatibility-verifier.enabled设置为false,如果要覆盖兼容的Spring Boot版本,只需使用逗号分隔的兼容Spring Boot版本列表设置spring.cloud.compatibility-verifier.compatible-boot-versions属性。上一篇:Spring Cloud Context:应用程序上下文服务 ...

April 12, 2019 · 3 min · jiezi

Java异常处理12条军规

摘要: 简单实用的建议。原文:Java异常处理12条军规公众号:Spring源码解析Fundebug经授权转载,版权归原作者所有。在Java语言中,异常从使用方式上可以分为两大类:CheckedExceptionUncheckedException在Java中类的异常结构图如下:可检查异常需要在方法上声明,一般要求调用者必须感知异常可能发生,并且对可能发生的异常进行处理。可以理解成系统正常状态下很可能发生的情况,通常发生在通过网络调用外部系统或者使用文件系统时,在这种情况下,错误是可能恢复的,调用者可以根据异常做出必要的处理,例如重试或者资源清理等。非检查异常是不需要在throws子句中声明的异常。JVM根本不会强制您处理它们,因为它们主要是由于程序错误而在运行时生成的。它们扩展了RuntimeException。最常见的例子是NullPointerException 可能不应该重试未经检查的异常,并且正确的操作通常应该是什么都不做,并让它从您的方法和执行堆栈中出来。在高执行级别,应记录此类异常。Error是最为严重的运行时错误,几乎是不可能恢复和处理,一些示例是OutOfMemoryError,LinkageError和StackOverflowError。它们通常会使程序或程序的一部分崩溃。只有良好的日志记录练习才能帮助您确定错误的确切原因.在异常处理时的几点建议:1. 永远不要catch中吞掉异常,否则在系统发生错误时,你永远不知道到底发生了什么catch (SomeException e) { return null;}2. 尽量使用特定的异常而不是一律使用Exception这样太泛泛的异常public void foo() throws Exception { //错误的做法}public void foo() throws MyBusinessException1, MyBusinessException2 { //正确的做法}一味的使用Exception,这样就违背了可检查异常的设计初衷,因为调用都不知道Exception到底是什么,也不知道该如何处理。捕获异常时,也不要捕获范围太大,例如捕获Exception,相反,只捕获你能处理的异常,应该处理的异常。即然方法的声明者在方法上声明了不同类型的可检查异常,他是希望调用者区别对待不同异常的。3. Never catch Throwable class永远不要捕获Throwable,因为Error也是继承自它,Error是Jvm都处理不了的错误,你能处理?所以基于有些Jvm在Error时就不会让你catch住。4. 正确的封装和传递异常**不要丢失异常栈,因为异常栈对于定位原始错误很关键catch (SomeException e) {throw new MyServiceException(“Some information: " + e.getMessage()); //错误的做法}一定要保留原始的异常:catch (SomeException e) { throw new MyServiceException(“Some information: " , e); //正确的打开方式}5. 要打印异常,就不要抛出,不要两者都做catch (SomeException e) { LOGGER.error(“Some information”, e); throw e;}这样的log没有任何意义,只会打印出一连串的error log,对于定位问题无济于事。6. 不要在finally块中抛出异常如果在finally中抛出异常,将会覆盖原始的异常,如果finally中真的可能会发生异常,那一定要处理并记录它,不要向上抛。7. 不要使用printStackTrace要给异常添加上有用的上下文信息,单纯的异常栈,没有太大意义8. Throw early catch late异常界著名的原则,错误发生时及早抛出,然后在获得所以全部信息时再捕获处理.也可以理解为在低层次抛出的异常,在足够高的抽象层面才能更好的理解异常,然后捕获处理。9. 对于使用一些重量级资源的操作,发生异常时,一定记得清理如网络连接,数据库操作等,可以用try finally来做clean up的工作。10. 不要使用异常来控制程序逻辑流程我们总是不经意间这么做了,这样使得代码变更丑陋,使得正常业务逻辑和错误处理混淆不清;而且也可能会带来性能问题,因为异常是个比较重的操作。11. 及早校验用户的输入在最边缘的入口校验用户的输入,这样使得我们不用再更底层逻辑中处处校验参数的合法性,能大大简化业务逻辑中不必要的异常处理逻辑;相反,在业务中不如果担心参数的合法性,则应该使用卫语句抛出运行时异常,一步步把对参数错误的处理推到系统的边缘,保持系统内部的清洁。12. 在打印错误的log中尽量在一行中包含尽可能多的上下文LOGGER.debug(“enter A”);LOGGER.debug(“enter B”); //错误的方式LOGGER.debug(“enter A, enter B”);//正确的方式 ...

April 12, 2019 · 1 min · jiezi

状态机在马蜂窝机票订单交易系统中的应用与优化实践

在设计交易系统时,稳定性、可扩展性、可维护性都是我们需要关注的重点。本文将对如何通过状态机在交易系统中的应用解决上述问题做出一些探讨。关于马蜂窝机票订单交易系统交易系统往往存在订单维度多、状态多、交易链路长、流程复杂等特点。以马蜂窝大交通业务中的机票交易为例,用户提交的一个订单除了机票信息之外可能还包含很多信息,比如保险或者其他附加产品。其中保险又分为很多类型,如航意险、航延险、组合险等。从用户的维度看,一个订单是由购买的主产品机票和附加产品共同构成,支付的时候是作为一个整体去支付,而如果想要退票、退保也是可以部分操作的;从供应商的维度看,一个订单中的每个产品背后都有独立的供应商,机票有机票的供应商,保险有保险的供应商,每个供应商的订单都需要分开出票、独立结算。用户的购买支付流程、供应商的出票出保流程,构成一个有机的整体穿插在机票交易系统中,密不可分。状态机在机票交易系统中的应用与优化有限状态机的概念有限状态机(以下简称状态机)是一种用于对事物或者对象行为进行建模的工具。状态机将复杂的逻辑简化为有限个稳定状态,构建在这些状态之间的转移和动作等行为的数学模型,在稳定状态中判断事件。对状态机输入一个事件,状态机会根据当前状态和触发的事件唯一确定一个状态迁移。图1:FSM工作原理业务系统的本质就是描述真实的世界,因此几乎所有的业务系统中都会有状态机的影子。订单交易流程更是天然适合状态机模型的应用。以用户支付流程为例,如果不使用状态机,在接收到支付成功回调时则需要执行一系列动作:查询支付流水号、记录支付时间、修改主订单状态为已支付、通知供应商去出票、记录通知出票时间、修改机票子订单状态为出票中…… 逻辑非常繁琐,而且代码耦合严重。为了使交易系统的订单状态按照设计流程正确向下流转,比如当前用户已支付,不允许再支付;当前订单已经关单,不能再通知出票等等,我们通过应用状态机的方式来优化机票交易系统,将所有的状态、事件、动作都抽离出来,对复杂的状态迁移逻辑进行统一管理,来取代冗长的 if else 判断,使机票交易系统中的复杂问题得以解耦,变得直观、方便操作,使系统更加易于维护和管理。状态机设计在数据库设计层面,我们将整个订单整体作为一个主订单,把供应商的订单作为子订单。假设一个用户同时购买了机票和保险,因为机票、保险对应的是不同的供应商,也就是 1 个主订单 order 对应 2 个子订单 sub_order。其中主订单 order 记录用户的信息(UID、联系方式、订单总价格等),子订单 sub_order 记录产品类型、供应商订单号、结算价格等。同时,我们把正向出票、逆向退票改签分开,抽成不同的子系统。这样每个子系统都是完全独立的,有利于系统的维护和拓展。对于机票正向子系统而言,有两套状态机:主订单状态机负责管理 order 的状态,包括创单成功、支付成功、交易成功、订单关闭等;子订单状态机负责管理 sub_order 的状态,维护预订成功到出票的流程。同样,对于逆向退票和改签子系统,也会有各自的状态机。图2:机票主订单状态机状态转移示例框架选型目前业界常用的状态机引擎框架主要有 Spring Statemachine、Stateless4j、Squirrel-Foundation 等。经过结合实际业务进行横向对比后,最终我们决定使用 Squirrel-Foundation,主要是因为:代码量适中,扩展和维护相对而言比较容易;StateMachine 轻量,实例创建开销小;切入点丰富,支持状态进入、状态完成、异常等节点的监听,使转换过程留有足够的切入点;支持使用注解定义状态转移,使用方便;从设计上不支持单例复用,只能随用随 New,因此状态机的本身的生命流管理很清晰,不会因为状态机单例复用的问题造成麻烦。 MSM 的设计与实现结合大交通业务逻辑,我们在 Squirrel-Foundation 的基础之上进行了 Action 概念的抽取和二次封装,将状态迁移、异步消息糅合到一起,封装成为 MSM 框架 (MFW State Machine),用来实现业务订单状态定义、事件定义和状态机定义,并用注解的形式来描述状态迁移。我们认为一次状态迁移必然会伴随着异步消息,因此把一个流程中必须要成功的数据库操作放到一个事务中,把允许失败重试并且对实时度要求不高的操作放到异步消息消费的流程中。以机票订单支付成功为例,机票订单支付成功时,会涉及修改订单状态为已支付、更新支付流水号等,这些是在一个事务中;而通知供应商出票,则是放在异步消息消费中处理。异步消息的实现使用的是 RocketMQ,主要考虑到 RocketMQ 支持二阶段提交,消息可靠性有保证,支持重试,支持多个 Consumer 组。以下具体说明:1. 对每个状态迁移需要执行的动作,都会抽取出一个Action 类,并且继承 AbstractAction,支持多个不同的状态迁移执行相同的动作。这里主要取决于 public List<ActionCondition> matchConditions() 的实现,因此只需要 matchConditions 返回多个初始状态-事件的匹配条件键值对就可以了。每个 Action 都有一个对应的继承 MFWContext 类的上下文类,用于在 process saveDB 等方法中的通信。2. 注册所有的 Action,添加每个状态迁移执行完成或者执行失败的监听。3. 由于依赖 RocketMQ 异步消息,所以需要一个 Spring Bean 去继承 BaseMessageSender,这个类会生成异步消息提供者。如果要使用二阶段提交,则需要一个类继承 BaseMsgTransactionListener,这里可以参考机票的 OrderChangeMessageSender 和 OrderChangeMsgTransactionListener。4. 最后,实现一个事件触发器类。在这个类里面包含一个 Apply 方法,传入订单 PO 对象、事件、对应的上下文,每次执行都实例化出一个状态机实例,并初始化当前状态,并调用 Fire 方法。5. 实例化一个状态机对象,设置当前状态为数据库对应的状态,调用 Fire 方法之后,最终会执行到 OrderStateMachine 类里面用注解描述的 callMethod 方法。我们配置的是 callMethod = “action”,它就会反射执行当前类的 Action 方法。Action 方法我们的实现是通过 super.action(from, to, event, context),就会执行 MFWStateMachine 的 Action 方法,先去根据当前状态和事件获取对应的Action,这里使用到了「工厂模式」,然后执行 Process 方法。如果成功,会执行在 MFWStateMachine 类初始化的 TransitionCompleteListener,执行该 Action的 afterProcess 方法来修改数据库记录以及发送消息;如果失败,会执行TransitionExceptionListener,执行该 Action 的onException 方法来进行相应处理。综上,MSM 可以根据 Action 类的声明和配置,来动态生成出 Squirrel-Foundation 的状态机定义,而不需要由使用方再去定义一次,使 MSM 的使用更方便。图3: UML趟过的坑1. 事务不生效最初我们使用 Spring 注解方式进行事务管理,即在 Action 类的数据库操作方法上加 @Transactional 注解,却发现在实践中不起作用。经过排查后发现, Spring 的事务注解是靠 AOP 切面实现的。在对象内部的方法中调用该对象其他使用 AOP 注解的方法,被调用方法的 AOP 注解会失效。因为同一个类的内部代码调用中,不会走代理类。后来我们通过手动开启事务的方式来解决此问题。2. 匹配 Action 最初我们匹配 Action 有两种方式:精准匹配及非精准匹配。精准匹配是指只有当某个状态迁移的初始状态和触发的事件一致时,才能匹配到 Action;非精准匹配是指只要触发的事件一致,就可以匹配到 Action。后来我们发现非精准匹配在某些情形下会出现问题,于是统一改成了多条件精准匹配。即在执行状态机触发时执行的 Action 方法时,去精准匹配 Action,多个状态迁移执行的方法可以匹配到同一个 Action,这样能够复用 Action 代码而不会出问题。 3. 异步消息一致性 有一些情况是绝不能出现的,比如修改数据库没成功即发出了消息;或是修改数据库成功了,而发送消息失败;或是在提交数据库事务之前,消息已经发送成功了。解决这个问题我们用到了 RocketMQ 的事务消息功能,它支持二阶段提交,会先发送一条预处理消息,然后回调执行本地事务,最终提交或者回滚,帮助保证修改数据库的信息和发送异步消息的一致。4. 同一条订单数据并发执行不同事件 在某些情况下,同一条订单数据可能会在同一时间(毫秒级)同时触发不同的事件。如机票主订单在待支付状态下,可以接收支付中心的回调,触发支付成功事件;也可以由用户点击取消订单,或者超时未支付定时任务来触发关单事件。如果不做任何控制的话,一个订单将可能出现既支付成功又会被取消。我们用数据库乐观锁来规避这个问题:在执行修改数据库的事务时,update 订单的语句带有原状态的条件判断,通过判断更新行数是否为 1,来决定是否抛出异常,即生成这样的 SQL 语句:update order where order_id = ‘1234’ and order_status = ‘待支付’。这样的话,如果两个事件同时触发同时执行,谁先把事务提交成功,谁就能执行成功;事务提交较晚的事件会因为更新行数为 0 而执行失败,最终回滚事务,就仿佛无事发生过一样。使用悲观锁也可以解决这个问题,这种方式是谁先争抢到锁谁就可以成功执行。但考虑到可能会有脚本对数据库批量修改,悲观锁存在死锁的潜在问题,我们最终还是采用了乐观锁的方式。总结MSM 状态机的定义和声明在 Squirrel-Foundation 的基础之上,抽取出 Action 概念,并对 Action 类配置起始状态、目标状态、触发的事件、上下文定义等,使 MSM 可以根据 Action 类的声明和配置,来动态生成出 Squirrel-Foundation 的状态机定义,而不需要使用方再去定义一次,操作更简单,维护起来也更容易。 通过使用状态机,机票订单交易系统的流程复杂性问题迎刃而解,系统在稳定性、可扩展性、可维护性等方面也得到了显著的改善和提升。状态机的使用场景不仅仅局限于订单交易系统,其他一些涉及到状态变更的复杂流程的系统也同样适用。希望通过本文的介绍,能使有状态机了解和使用需求的读者朋友有所收获。本文作者:董天,马蜂窝大交通研发团队机票交易系统研发工程师。(马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,谢谢配合。)关注马蜂窝技术,找到更多你想要的内容 ...

April 12, 2019 · 1 min · jiezi

一起来读Spring源码吧(四)循环依赖踩坑笔记

源起在开发过程中,遇到需要把方法调用改为异步的情况,本来以为简单得加个@Asyn在方法上就行了,没想到项目启动的时候报了如下的错误:Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘customerServiceImpl’: Bean with name ‘customerServiceImpl’ has been injected into other beans [customerServiceImpl,followServiceImpl,cupidService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.看了下好像报的是循环依赖的错误,但是Spring单例是支持循环依赖的,当时一脸懵逼。拿着报错去百度了下,说是多个动态代理导致的循环依赖报错,也找到了报错的地点,但是还是不明白为什么会这样,所以打算深入源码探个究竟,顺便回顾下Bean的获取流程和循环依赖的内容。模拟场景用SpringBoot新建一个demo项目,因为原项目是有定义切面的,这里也定义一个切面:@Aspect@Componentpublic class TestAspect { @Pointcut(“execution(public * com.example.demo.service.CyclicDependencyService.sameClassMethod(..))”) private void testPointcut() {} @AfterReturning(“testPointcut()”) public void after(JoinPoint point) { System.out.println(“在” + point.getSignature() + “之后干点事情”); }}然后新建一个注入自己的Service构成循环依赖,然后提供一个方法满足切点要求,并且加上@Async注解:@Servicepublic class CyclicDependencyService { @Autowired private CyclicDependencyService cyclicDependencyService; public void test() { System.out.println(“调用同类方法”); cyclicDependencyService.sameClassMethod(); } @Async public void sameClassMethod() { System.out.println(“循环依赖中的异步方法”); System.out.println(“方法线程:” + Thread.currentThread().getName()); }}还有别忘了给Application启动类加上@EnableAsync和@EnableAspectJAutoProxy:@EnableAsync@EnableAspectJAutoProxy@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}最后打好断点,开始debug。debug从Bean创建的的起点–AbstractBeanFactory#getBean开始// Eagerly check singleton cache for manually registered singletons.Object sharedInstance = getSingleton(beanName);首先会在缓存中查找,DefaultSingletonBeanRegistry#getSingleton(String beanName, boolean allowEarlyReference):protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject;}这里一共有三级缓存:singletonObjects,保存初始化完成的单例bean实例;earlySingletonObjects,保存提前曝光的单例bean实例;singletonFactories,保存单例bean的工厂函数对象;后面两级都是为了解决循环依赖设置的,具体查找逻辑在后续其他情况下调用会说明。缓存中找不到,就要创建单例:sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; }});调用DefaultSingletonBeanRegistry#getSingleton(String beanName, ObjectFactory<?> singletonFactory):public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { … beforeSingletonCreation(beanName); … singletonObject = singletonFactory.getObject(); … afterSingletonCreation(beanName); … addSingleton(beanName, singletonObject); …}创建前后分别做了这几件事:前,beanName放入singletonsCurrentlyInCreation,表示单例正在创建中后,从singletonsCurrentlyInCreation中移除beanName后,将创建好的bean放入singletonObjects,移除在singletonFactories和earlySingletonObjects的对象创建单例调用getSingleton时传入的工厂函数对象的getObject方法,实际上就是createBean方法,主要逻辑在AbstractAutowireCapableBeanFactory#doCreateBean中:…instanceWrapper = createBeanInstance(beanName, mbd, args);final Object bean = instanceWrapper.getWrappedInstance();…// Eagerly cache singletons to be able to resolve circular references// even when triggered by lifecycle interfaces like BeanFactoryAware.boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) { if (logger.isTraceEnabled()) { logger.trace(“Eagerly caching bean ‘” + beanName + “’ to allow for resolving potential circular references”); } addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}// Initialize the bean instance.Object exposedObject = bean;try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd);}…if (earlySingletonExposure) { Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { if (exposedObject == bean) { exposedObject = earlySingletonReference; } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } if (!actualDependentBeans.isEmpty()) { throw new BeanCurrentlyInCreationException(beanName, “Bean with name ‘” + beanName + “’ has been injected into other beans [” + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + “] in its raw version as part of a circular reference, but has eventually been " + “wrapped. This means that said other beans do not use the final version of the " + “bean. This is often the result of over-eager type matching - consider using " + “‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.”); } } }}可以看到报错就是在这个方法里抛出的,那么这个方法就是重点中的重点。首先实例化单例,instantiate,只是实例化获取对象引用,还没有注入依赖。我debug时记录的bean对象是CyclicDependencyService@4509;然后判断bean是否需要提前暴露,需要满足三个条件:1、是单例;2、支持循环依赖;3、bean正在创建中,也就是到前面提到的singletonsCurrentlyInCreation中能查找到,全满足的话就会调用DefaultSingletonBeanRegistry#addSingletonFactory把beanName和单例工厂函数对象(匿名实现调用AbstractAutowireCapableBeanFactory#getEarlyBeanReference方法)放入singletonFactories;接着就是注入依赖,填充属性,具体怎么注入这里就不展开了,最后会为属性cyclicDependencyService调用DefaultSingletonBeanRegistry.getSingleton(beanName, true),注意这里和最开始的那次调用不一样,isSingletonCurrentlyInCreation为true,就会在singletonFactories中找到bean的单例工厂函数对象,也就是在上一步提前暴露时放入的,然后调用它的匿名实现AbstractAutowireCapableBeanFactory#getEarlyBeanReference:protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject;}方法逻辑就是挨个调用实现了SmartInstantiationAwareBeanPostProcessor接口的后置处理器(以下简称BBP)的getEarlyBeanReference方法。一个一个debug下来,其他都是原样返回bean,只有AnnotationAwareAspectJAutoProxyCreator会把原bean(CyclicDependencyService@4509)存在earlyProxyReferences,然后将bean的代理返回(debug时记录的返回对象是CyclicDependencyService$$EnhancerBySpringCGLIB$$6ed9e2db@4740)并放入earlySingletonObjects,再赋给属性cyclicDependencyService。public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey = getCacheKey(bean.getClass(), beanName); this.earlyProxyReferences.put(cacheKey, bean); return wrapIfNecessary(bean, beanName, cacheKey);}属性填充完成后就是调用初始化方法AbstractAutowireCapableBeanFactory#initializeBean:…invokeAwareMethods(beanName, bean);…wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);…invokeInitMethods(beanName, wrappedBean, mbd);…wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);…初始化主要分为这几步:如果bean实现了BeanNameAware、BeanClassLoaderAware或BeanFactoryAware,把相应的资源放入bean;顺序执行BBP的postProcessBeforeInitialization方法;如果实现了InitializingBean就执行afterPropertiesSet方法,然后执行自己的init-method;顺序执行BBP的postProcessAfterInitialization。debug的时候发现是第4步改变了bean,先执行AnnotationAwareAspectJAutoProxyCreator#postProcessAfterInitialization:public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean;}这里会获取并移除之前存在earlyProxyReferences的bean(CyclicDependencyService@4509),因为和当前bean是同一个对象,所以什么都没做直接返回。随后会执行AsyncAnnotationBeanPostProcessor#postProcessAfterInitialization:if (isEligible(bean, beanName)) { ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); if (!proxyFactory.isProxyTargetClass()) { evaluateProxyInterfaces(bean.getClass(), proxyFactory); } proxyFactory.addAdvisor(this.advisor); customizeProxyFactory(proxyFactory); return proxyFactory.getProxy(getProxyClassLoader());}先判断bean是否有需要代理,因为CyclicDependencyService有方法带有@Async注解就需要代理,返回代理对象是CyclicDependencyService$$EnhancerBySpringCGLIB$$e66d8f6e@5273。返回的代理对象赋值给AbstractAutowireCapableBeanFactory#doCreateBean方法内的exposedObject,接下来就到了检查循环依赖的地方了:if (earlySingletonExposure) { Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { if (exposedObject == bean) { exposedObject = earlySingletonReference; } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } if (!actualDependentBeans.isEmpty()) { throw new BeanCurrentlyInCreationException(beanName, “Bean with name ‘” + beanName + “’ has been injected into other beans [” + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + “] in its raw version as part of a circular reference, but has eventually been " + “wrapped. This means that said other beans do not use the final version of the " + “bean. This is often the result of over-eager type matching - consider using " + “‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.”); } } }}首先从earlySingletonObjects里拿到前面属性填充时放入的bean代理(CyclicDependencyService$$EnhancerBySpringCGLIB$$6ed9e2db@4740),不为空的话就比较bean和exposedObject,分别是CyclicDependencyService@4509和CyclicDependencyService$$EnhancerBySpringCGLIB$$e66d8f6e@5273,很明显不是同一个对象,然后会判断allowRawInjectionDespiteWrapping属性和是否有依赖的bean,然后判断这些bean是否是真实依赖的,一旦存在真实依赖的bean,就会抛出BeanCurrentlyInCreationException。总结总结下Spring解决循环依赖的思路:在创建bean时,对于满足提前曝光条件的单例,会把该单例的工厂函数对象放入三级缓存中的singletonFactories中;然后在填充属性时,如果存在循环依赖,必然会尝试获取该单例,也就是执行之前放入的工厂函数的匿名实现,这时候拿到的有可能是原bean对象,也有可能是被某些BBP处理过返回的代理对象,会放入三级缓存中的earlySingletonObjects中;接着bean开始初始化,结果返回的有可能是原bean对象,也有可能是代理对象;最后对于满足提前曝光的单例,如果真的有提前曝光的动作,就会去检查初始化后的bean对象是不是原bean对象是同一个对象,只有不是的情况下才可能抛出异常。重点就在于存在循环依赖的情况下,初始化过的bean对象是不是跟原bean是同一个对象。从以上的debug过程可以看出,是AsyncAnnotationBeanPostProcessor这个BBP在初始化过程中改变了bean,使得结果bean和原bean不是一个对象,而AnnotationAwareAspectJAutoProxyCreator则是在填充属性获取提前曝光的对象时把原始bean缓存起来,返回代理的bean。然后在初始化时执行它的postProcessAfterInitialization方法时如果传入的bean是之前缓存的原始bean,就直接返回,不进行代理。如果其他BBP也都没有改变bean的话,初始化过后的bean就是跟原始bean是同一个对象,这时就会把提前曝光的对象(代理过的)作为最终生成的bean。 ...

April 11, 2019 · 4 min · jiezi

Spring Cloud Alibaba基础教程:使用Sentinel实现接口限流

最近管点闲事浪费了不少时间,感谢网友libinwalan的留言提醒。及时纠正路线,继续跟大家一起学习Spring Cloud Alibaba。Nacos作为注册中心和配置中心的基础教程,到这里先告一段落,后续与其他结合的内容等讲到的时候再一起拿出来说,不然内容会有点跳跃。接下来我们就来一起学习一下Spring Cloud Alibaba下的另外一个重要组件:Sentinel。Sentinel是什么Sentinel的官方标题是:分布式系统的流量防卫兵。从名字上来看,很容易就能猜到它是用来作服务稳定性保障的。对于服务稳定性保障组件,如果熟悉Spring Cloud的用户,第一反应应该就是Hystrix。但是比较可惜的是Netflix已经宣布对Hystrix停止更新。那么,在未来我们还有什么更好的选择呢?除了Spring Cloud官方推荐的resilience4j之外,目前Spring Cloud Alibaba下整合的Sentinel也是用户可以重点考察和选型的目标。Sentinel的功能和细节比较多,一篇内容很难介绍完整。所以下面我会分多篇来一一介绍Sentinel的重要功能。本文就先从限流入手,说说如何把Sentinel整合到Spring Cloud应用中,以及如何使用Sentinel Dashboard来配置限流规则。通过这个简单的例子,先将这一套基础配置搭建起来。使用Sentinel实现接口限流Sentinel的使用分为两部分:sentinel-dashboard:与hystrix-dashboard类似,但是它更为强大一些。除了与hystrix-dashboard一样提供实时监控之外,还提供了流控规则、熔断规则的在线维护等功能。客户端整合:每个微服务客户端都需要整合sentinel的客户端封装与配置,才能将监控信息上报给dashboard展示以及实时的更改限流或熔断规则等。下面我们就分两部分来看看,如何使用Sentienl来实现接口限流。部署Sentinel Dashboard本文采用的spring cloud alibaba版本是0.2.1,可以查看依赖发现当前版本使用的是sentinel 1.4.0。为了顺利完成本文的内容,我们可以挑选同版本的sentinel dashboard来使用是最稳妥的。下载地址:https://github.com/alibaba/Se…其他版本:https://github.com/alibaba/Se…同以往的Spring Cloud教程一样,这里也不推荐大家跨版本使用,不然可能会出现各种各样的问题。通过命令启动:java -jar sentinel-dashboard-1.4.0.jarsentinel-dashboard不像Nacos的服务端那样提供了外置的配置文件,比较容易修改参数。不过不要紧,由于sentinel-dashboard是一个标准的spring boot应用,所以如果要自定义端口号等内容的话,可以通过在启动命令中增加参数来调整,比如:-Dserver.port=8888。默认情况下,sentinel-dashboard以8080端口启动,所以可以通过访问:localhost:8080来验证是否已经启动成功,如果一切顺利的话,可以看到如下页面:整合Sentinel第一步:在Spring Cloud应用的pom.xml中引入Spring Cloud Alibaba的Sentinel模块: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.2</version> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>第二步:在Spring Cloud应用中通过spring.cloud.sentinel.transport.dashboard参数配置sentinel dashboard的访问地址,比如:spring.application.name=alibaba-sentinel-rate-limitingserver.port=8001# sentinel dashboardspring.cloud.sentinel.transport.dashboard=localhost:8080第三步:创建应用主类,并提供一个rest接口,比如:@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @Slf4j @RestController static class TestController { @GetMapping("/hello") public String hello() { return “didispace.com”; } }}第四步:启动应用,然后通过postman或者curl访问几下localhost:8001/hello接口。$ curl localhost:8001/hellodidispace.com此时,在上一节启动的Sentinel Dashboard中就可以当前我们启动的alibaba-sentinel-rate-limiting这个服务以及接口调用的实时监控了。具体如下图所示:配置限流规则在完成了上面的两节之后,我们在alibaba-sentinel-rate-limiting服务下,点击簇点链路菜单,可以看到如下界面:其中/hello接口,就是我们上一节中实现并调用过的接口。通过点击流控按钮,来为该接口设置限流规则,比如:这里做一个最简单的配置:阈值类型选择:QPS单机阈值:2综合起来的配置效果就是,该接口的限流策略是每秒最多允许2个请求进入。点击新增按钮之后,可以看到如下界面:其实就是左侧菜单中流控规则的界面,这里可以看到当前设置的所有限流策略。验证限流规则在完成了上面所有内容之后,我们可以尝试一下快速的调用这个接口,看看是否会触发限流控制,比如:$ curl localhost:8001/hellodidispace.com$ curl localhost:8001/hellodidispace.com$ curl localhost:8001/helloBlocked by Sentinel (flow limiting)可以看到,快速的调用两次/hello接口之后,第三次调用被限流了。代码示例本文介绍内容的客户端代码,示例读者可以通过查看下面仓库中的alibaba-sentinel-rate-limiting项目:Github:https://github.com/dyc87112/SpringCloud-Learning/Gitee:https://gitee.com/didispace/SpringCloud-Learning/如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!参考资料下面是Sentinel的仓库地址与官方文档,读者也可以自己查阅文档学习:GithubSentinel官方文档Spring Cloud Alibaba Sentinel文档专题推荐Spring Boot基础教程Spring Cloud基础教程 ...

April 11, 2019 · 1 min · jiezi

Spring Security 进阶-细节总结

关于 Spring Security 的学习已经告一段落了,刚开始接触该安全框架感觉很迷茫,总觉得没有 Shiro 灵活,到后来的深入学习和探究才发现它非常强大。简单快速集成,基本不用写任何代码,拓展起来也非常灵活和强大。系统集成集成完该框架默认情况下,系统帮我们生成一个登陆页,默认除了登陆其他请求都需要进行身份认证,没有身份认证前的任何操作都会跳转到默认登录页。默认生成的密码也会在控制台输出。简单页面自定义接下来我们可能需要自己控制一下权限,自定义一下登录界面@Overrideprotected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .formLogin() .loginPage("/login.html") //自定义登录界面 .loginProcessingUrl("/login.action") //指定提交地址 .defaultSuccessUrl("/main.html") //指定认证成功跳转界面 //.failureForwardUrl("/error.html") //指定认证失败跳转界面(注: 转发需要与提交登录请求方式一致) .failureUrl("/error.html") //指定认证失败跳转界面(注: 重定向需要对应的方法为 GET 方式) .usernameParameter(“username”) //username .passwordParameter(“password”) //password .permitAll() .and() .logout() .logoutUrl("/logout.action") //指定登出的url, controller里面不用写对应的方法 .logoutSuccessUrl("/login.html") //登出成功跳转的界面 .permitAll() .and() .authorizeRequests() .antMatchers("/register*").permitAll() //设置不需要认证的 .mvcMatchers("/main.html").hasAnyRole(“admin”) .anyRequest().authenticated() //其他的全部需要认证 .and() .exceptionHandling() .accessDeniedPage("/error.html"); //配置权限失败跳转界面 (注: url配置不会被springmvc异常处理拦截, 但是注解配置springmvc异常机制可以拦截到)}从上面配置可以看出自定义配置可以简单地分为四个模块(登录页面自定义、登出自定义、权限指定、异常设定),每个模块都对应着一个过滤器,详情请看 Spring Security 进阶-原理篇需要注意的是:配置登录提交的URL loginProcessingUrl(..)、登出URL logoutUrl(..) 都是对应拦截器的匹配地址,会在对应的过滤器里面执行相应的逻辑,不会执行到 Controller 里面的方法。配置的登录认证成功跳转的URL defaultSuccessUrl(..)、登录认证失败跳转的URL failureUrl(..)、登录认证失败转发的URL failureForwardUrl(..)……以及下面登出和权限配置的URL 可以是静态界面地址,也可以是 Controller 里面对应的方法。这里配置 URL 对应的访问权限,访问失败不会被 SpringMVC 的异常方法拦截到,注解配置的可以被拦截到。但是我们最好不要在 SpringMVC 里面对他进行处理,而是放到配置的权限异常来处理。登录身份认证失败跳转对应的地址前会把异常保存到 request(转发) 或 session(重定向) 里面,可以通过 key WebAttributes.AUTHENTICATION_EXCEPTION 来取出,但是前提是使用系统提供的身份认证异常处理handler SimpleUrlAuthenticationFailureHandler。上面这种配置身份认证失败都会跳转到登录页,权限失败会跳转指定的 URL,没有配置 URL 则会响应 403 的异常给前端,前提是在使用系统为我们提供的默认权限异常处理handler AccessDeniedHandlerImpl。异步响应配置大多数开发情况下都是前后端分离,响应也都是异步的,不是上面那种表单界面的响应方式,虽然通过上面跳转到URL对应的 Controller 里面的方法也能解决,但是大多数情况下我们需要的是极度简化,这时候一些自定义的处理 handler 就油然而生。@Overrideprotected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .formLogin() .loginProcessingUrl("/login") .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("****** 身份认证成功 "); response.setStatus(HttpStatus.OK.value()); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println(" 身份认证失败 "); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } }) .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println(" 登出成功 "); response.setStatus(HttpStatus.OK.value()); } }) .permitAll() .and() .authorizeRequests() .antMatchers("/main").hasAnyRole(“admin”) .and() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { System.out.println(" 没有进行身份认证 "); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } }) .accessDeniedHandler(new AccessDeniedHandler() { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { System.out.println(" 没有权限 "); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } });}注意:没有指定登录界面,那么久需要至少配置两个 handler,登出失败 hander logoutSuccessHandler(..),登录身份认证失败的 handler failureHandler(..),以免默认这样两个步骤向不存在的登录页跳转。配置的登录身份认证失败 handler failureHandler(..) 和 没有进行身份认证的异常 handler authenticationEntryPoint(..),这两个有区别,前者是在认证过程中出现异常处理,后者是在访问需要进行身份认证的URL时没有进行身份认证异常处理。自定义身份认证过程开发的时候我们需要自己来实现登录登出的流程,下面来个最简单的自定义。@Overrideprotected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .logout() .logoutUrl("/logout") .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println(" 登出成功 "); response.setStatus(HttpStatus.OK.value()); } }) .permitAll() .and() .authorizeRequests() .antMatchers("/main").hasAnyRole(“admin”) .and() .exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { System.out.println(" 没有进行身份认证 "); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } }) .accessDeniedHandler(new AccessDeniedHandler() { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { System.out.println(" 没有权限 "); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } }) .and() .addFilterBefore(new LoginFilter(), UsernamePasswordAuthenticationFilter.class);}注意:这里配置了登出,也可以不配置,在自定义登出的 Controller 方法里面进行手动清空 SecurityContextHolder.clearContext();,但是建议配置,一般登录和登出最好都在过滤器里面进行处理。添加自定义登录过滤器,相当于配置登录。记得配置登录认证前和过程中的一些请求不需要身份认证。自定义登录过滤器详情public class LoginFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; if ("/login".equals(httpServletRequest.getServletPath())) { //开始登录过程 String username = httpServletRequest.getParameter(“username”); String password = httpServletRequest.getParameter(“password”); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password); //模拟数据库查出来的 User.UserBuilder userBuilder = User.withUsername(username); userBuilder.password(“123”); userBuilder.roles(“user”, “admin”); UserDetails user = userBuilder.build(); if (user == null) { System.out.println(" 自定义登录过滤器 该用户不存在 "); httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } if (!user.getUsername().equals(authentication.getPrincipal())) { System.out.println(" 自定义登录过滤器 账号有问题 "); httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } if (!user.getPassword().equals(authentication.getCredentials())) { System.out.println(" 自定义登录过滤器 密码有问题 ******"); httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user.getUsername(), authentication.getCredentials(), user.getAuthorities()); result.setDetails(authentication.getDetails()); //注: 最重要的一步 SecurityContextHolder.getContext().setAuthentication(result); httpServletResponse.setStatus(HttpStatus.OK.value()); } else { chain.doFilter(request, response); } }}注意:不是登录认证就接着执行下一个过滤器或其他。登录认证失败不能直接抛出错误,需要向前端响应异常。完成登录逻辑直接响应,不需要接着往下执行什么。 ...

April 9, 2019 · 3 min · jiezi

记录Redis序列化的坑-存Long取Integer的类型转换错误问题及String对象被转义的问题

背景最近遇到了两个Redis相关的问题,趁着清明假期,梳理整理。1.存入Long类型对象,在代码中使用Long类型接收,结果报类型转换错误。2.String对象的反序列化问题,直接在Redis服务器上新增一个key-value,而后在代码中get(key)时,报反序列化失败。Long类型接收返回值报错的问题Redis的配置如下Redis中序列化相关的配置,我这里采用的是GenericJackson2JsonRedisSerializer类型的序列化方式(这种方式会有一个类型转换的坑,下面会提到)@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)public class RedisConfiguration { @Bean public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; }} 存入Long对象取出Integer对象测试方法如下@Testpublic void redisSerializerLong(){ try { Long longValue = 123L; redisLongCache.set(“cacheLongValue”,longValue); Object cacheValue = redisLongCache.get(“cacheLongValue”); Long a = (Long) cacheValue; }catch (ClassCastException e){ e.printStackTrace(); }}会报类型转换错误java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long。为什么类型会变为Integer呢?跟我一起追踪源码,便会发现问题。1. 在代码的最外层获取redis中key对应的value值redisTemplate.opsForValue().get(key);2.在DefaultValueOperations类中的get(Object key)方法public V get(Object key) { return execute(new ValueDeserializingRedisCallback(key) { @Override protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { return connection.get(rawKey); } }, true);}3.打断点继续往里跟,RedisTemplate中的execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline)方法里面,有一行关键代码。T result = action.doInRedis(connToExpose); 此为获取redis中对应的value值,并对其进行反序列化操作。4.在抽象类AbstractOperations<K, V>中,定义了反序列化操作,对查询结果result进行反序列化。public final V doInRedis(RedisConnection connection) { byte[] result = inRedis(rawKey(key), connection); return deserializeValue(result);}V deserializeValue(byte[] value)反序列化V deserializeValue(byte[] value) { if (valueSerializer() == null) { return (V) value; } return (V) valueSerializer().deserialize(value);}5.终于到了具体实现类GenericJackson2JsonRedisSerializerpublic Object deserialize(@Nullable byte[] source) throws SerializationException { return deserialize(source, Object.class);}实现反序列化方法,注意!这里统一将结果反序列化为Object类型,所以这里便是问题的根源所在,对于数值类型,取出后统一转为Object,导致泛型类型丢失,数值自动转为了Integer类型也就不奇怪了。public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws SerializationException { Assert.notNull(type, “Deserialization type must not be null! Pleaes provide Object.class to make use of Jackson2 default typing.”); if (SerializationUtils.isEmpty(source)) { return null; } try { return mapper.readValue(source, type); } catch (Exception ex) { throw new SerializationException(“Could not read JSON: " + ex.getMessage(), ex); }} String对象转义问题测试方法@Testpublic void redisSerializerString() { try { String stringValue = “abc”; redisStringCache.set(“codeStringValue”, stringValue); String cacheValue = redisStringCache.get(“codeStringValue”); // 序列化失败 String serverInsert = redisStringCache.get(“serverInsertValue”); if (Objects.equals(cacheValue, serverInsert)) { System.out.println(“serializer ok”); } else { System.out.println(“serializer err”); } } catch (Exception e) { e.printStackTrace(); }}提前在redis服务器上插入一个非Json格式的String对象直接在Redis服务器上使用set命令新增一对Key-Value,在代码中取出会反序列化失败。org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unrecognized token ‘abc’: was expecting (’true’, ‘false’ or ’null’) at [Source: (byte[])“abc”; line: 1, column: 7]; nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token ‘abc’: was expecting (’true’, ‘false’ or ’null’) at [Source: (byte[])“abc”; line: 1, column: 7] at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:132) at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:110) at org.springframework.data.redis.core.AbstractOperations.deserializeValue(AbstractOperations.java:334) at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184) at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:95) at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:48) 总结这个问题是因为,自己在测试的过程中,没有按照代码流程执行,想当然的认为,代码跑出来的结果和自己手动插入的结果是一样的。在相关的测试验证过程中应该严格的控制变量,不能凭借下意识的决断来操作,谨记软件之事——必作于细! ...

April 7, 2019 · 2 min · jiezi

SpringBoot 仿抖音短视频小程序开发(一)

一、项目简介模仿抖音做的一个短视频微信小程序,用SpringBoot搭建小程序后台,用SSM框架搭建短视频后台管理系统,小程序后台通过分布式zookeeper监听节点自动下载或删除短视频后台管理系统上传的视频。二、环境参数核心框架:SpringBoot、SSM数据库:MySQL、 HikariCP数据源、MyBatis逆向工程中间件:zookeeper,redis,swagger2前端框架: Bootstrap + Jquery、jqGrid分页组件音频处理: FFmpeg开发工具: IDEA 热门技术点 三、项目展示 功能: 小程序【注册登录注销】、【上传头像】、【上传作品】、【查看所有/单个短视频】、【点赞】、【关注某用户】、【短视频和BGM合并】、【留言评论回复】、【举报】、【下载短视频到手机】 四、数据库设计CREATE TABLE bgm ( id varchar(64) NOT NULL, author varchar(255) NOT NULL, name varchar(255) NOT NULL, path varchar(255) NOT NULL COMMENT ‘播放地址’, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE comments ( id varchar(20) NOT NULL, father_comment_id varchar(20) DEFAULT NULL, to_user_id varchar(20) DEFAULT NULL, video_id varchar(20) NOT NULL COMMENT ‘视频id’, from_user_id varchar(20) NOT NULL COMMENT ‘留言者,评论的用户id’, comment text NOT NULL COMMENT ‘评论内容’, create_time datetime NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘课程评论表’;CREATE TABLE search_records ( id varchar(64) NOT NULL, content varchar(255) NOT NULL COMMENT ‘搜索的内容’, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘视频搜索的记录表’;CREATE TABLE vuser ( id varchar(64) NOT NULL, username varchar(20) NOT NULL COMMENT ‘用户名’, password varchar(64) NOT NULL COMMENT ‘密码’, face_image varchar(255) DEFAULT NULL COMMENT ‘我的头像,如果没有默认给一张’, nickname varchar(20) NOT NULL COMMENT ‘昵称’, fans_counts int(11) DEFAULT ‘0’ COMMENT ‘我的粉丝数量’, follow_counts int(11) DEFAULT ‘0’ COMMENT ‘我关注的人总数’, receive_like_counts int(11) DEFAULT ‘0’ COMMENT ‘我接受到的赞美/收藏 的数量’, PRIMARY KEY (id), UNIQUE KEY id (id), UNIQUE KEY username (username)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE users_fans ( id varchar(64) NOT NULL, user_id varchar(64) NOT NULL COMMENT ‘用户’, fan_id varchar(64) NOT NULL COMMENT ‘粉丝’, PRIMARY KEY (id), UNIQUE KEY user_id (user_id,fan_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘用户粉丝关联关系表’;CREATE TABLE users_like_videos ( id varchar(64) NOT NULL, user_id varchar(64) NOT NULL COMMENT ‘用户’, video_id varchar(64) NOT NULL COMMENT ‘视频’, PRIMARY KEY (id), UNIQUE KEY user_video_rel (user_id,video_id) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘用户喜欢的/赞过的视频’;CREATE TABLE users_report ( id varchar(64) NOT NULL, deal_user_id varchar(64) NOT NULL COMMENT ‘被举报用户id’, deal_video_id varchar(64) NOT NULL, title varchar(128) NOT NULL COMMENT ‘类型标题,让用户选择,详情见 枚举’, content varchar(255) DEFAULT NULL COMMENT ‘内容’, userid varchar(64) NOT NULL COMMENT ‘举报人的id’, create_date datetime NOT NULL COMMENT ‘举报时间’, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘举报用户表’;CREATE TABLE videos ( id varchar(64) NOT NULL, user_id varchar(64) NOT NULL COMMENT ‘发布者id’, audio_id varchar(64) DEFAULT NULL COMMENT ‘用户使用音频的信息’, video_desc varchar(128) DEFAULT NULL COMMENT ‘视频描述’, video_path varchar(255) NOT NULL COMMENT ‘视频存放的路径’, video_seconds float(6,2) DEFAULT NULL COMMENT ‘视频秒数’, video_width int(6) DEFAULT NULL COMMENT ‘视频宽度’, video_height int(6) DEFAULT NULL COMMENT ‘视频高度’, cover_path varchar(255) DEFAULT NULL COMMENT ‘视频封面图’, like_counts bigint(20) NOT NULL DEFAULT ‘0’ COMMENT ‘喜欢/赞美的数量’, status int(1) NOT NULL COMMENT ‘视频状态:\r\n1、发布成功\r\n2、禁止播放,管理员操作’, create_time datetime NOT NULL COMMENT ‘创建时间’, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘视频信息表’; ...

April 7, 2019 · 2 min · jiezi

【极简版】SpringBoot+SpringData JPA 管理系统

前言只有光头才能变强。文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y在上一篇中已经讲解了如何从零搭建一个SpringBoot+SpringData JPA的环境,测试接口的时候也成功获取得到数据了。带你搭一个SpringBoot+SpringData JPA的Demo我的目的是做一个十分简易的管理系统,这就得有页面,下面我继续来讲讲我是怎么快速搭一个管理系统的。ps:由于是简易版,我的目的是能够快速搭建,而不在于代码的规范性。(所以在后面你可能会看到很多丑陋的代码)一、搭建管理系统1.1. 搭建页面在上一篇的最后,我们可以通过http://localhost:8887/user接口拿到我们User表所有的记录了。我们现在希望把记录塞到一个管理页面上(展示起来)。作为一个后端,我HTML+CSS实在是丑陋,于是我就去找了一份BootStrap的模板。首先,我进到bootStrap的官网,找到基本模板这一块:我们在里边可以看到挺多的模板的,这里选择一个控制台页面:于是,就把这份模板下载下来,在本地中运行起来试试看。官方给出的链接是下载整一份文档,我们找到想要的页面即可:于是我们将这两份文件单独粘贴在我们的项目中,发现这HTML文件需要bootstrap.css、bootstrap.js、jquery 的依赖(原来用的是相对路径,其实我们就是看看相对路径的文件在我们这有没有,如果没有,那就是我们需要的)。这里我们在CDN中找找,导入链接就行了。于是我们就将所缺的依赖替换成BootCDN的依赖,最重要的几个依赖如下:<link href=“https://cdn.bootcss.com/twitter-bootstrap/3.4.0/css/bootstrap.min.css" rel=“stylesheet”><script src=“https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script><script src=“https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>如无意外的话,我们也能在项目中正常打开页面。1.1.2 把数据塞到页面上把数据塞到页面上,有两种方案:要么就后端返回json给前端进行解析,要么就使用模板引擎。而我为了便捷,是不想写JS代码的。所以,我使用freemarker这个模板引擎。为什么这么多模板引擎,我选择这个?因为我只会这个!在SpringBoot下使用freemarker也是非常简单,首先,我们需要加入pom文件依赖:<!–freemarker–><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId></dependency>随后,在application.yml文件中,加入freemarker的配置: # freemarker配置 freemarker: suffix: .ftl request-context-attribute: request expose-session-attributes: true content-type: text/html check-template-location: true charset: UTF-8 cache: false template-loader-path: classpath:/templates这里我简单解释一下:freemarker的文件后缀名为.ftl,程序从/templates路径下加载我们的文件。于是乎,我将本来是.html的文件修改成.ftl文件,并放在templates目录下:接下来将我们Controller得到的数据,塞到Model对象中: /** * 得到所有用户 / @GetMapping(value = “/user”, produces = {“application/json;charset=UTF-8”}) public String getAllUser ( Model model) { List<User> allUser = userService.getAllUser(); model.addAttribute(“users”, allUser); return “/index”; }图片如下:在ftl文件中,我们只要判断数据是否存在,如果存在则在表格中遍历出数据就行了: <#if users?? && (users?size > 0)> <#list users as user> <tr> <td>${user.userId}</td> <td>${user.userNickname}</td> <td>${user.userEmail}</td> <td>${user.actiState}</td> <td><a href=“http://localhost:8887/deleteUser?id=${user.userId}">删除</a></td> </tr> </#list> <#else> <h3>还没有任何用户</h3> </#if>图片如下:删除的Controller代码如下:/* * 根据ID删除某个用户 */@GetMapping(value = “/deleteUser”, produces = {“application/json;charset=UTF-8”})public String deleteUserById (String id,Model model) { userService.deleteUserById(id); return getAllUser(model);}我们再找几张自己喜欢的图片,简单删除一些不必要模块,替换成我们想要的文字,就可以得到以下的效果了:至于图片上的评论管理、备忘录管理的做法都如上,我只是把文件再复制一次而已(期中没有写任何的JS代码,懒)。在编写的期中,要值得注意的是:静态的文件一般我们会放在static文件夹中。项目的目录结构如下:最后本文涉及到的链接(bootstrap & cdn):https://v3.bootcss.com/getting-started/#templatehttps://www.bootcdn.cn/all/乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,不妨来关注一下!觉得我的文章写得不错,不妨点一下赞! ...

April 6, 2019 · 1 min · jiezi

【译】Spring Boot 2.0的属性绑定

Spring Boot2.0的属性绑定原文从Spring boot第一个版本以来,我们可以使用@ConfigurationProperties注解将属性绑定到对象。也可以指定属性的各种不同格式。比如,person.first-name,person.firstName和PERSON_FIRSTNAME都可以使用。这个功能叫做“relaxed binding”。不幸的是,在spring boot 1.x,“relaxed binding”显得太随意了。从而使得很难来定义准确的绑定规则和指定使用的格式。在1.x的实现中,也很难对其进行修正。比如,在spring boot 1.x中,不能将属性绑定到java.util.Set对象。所以,在spring boot 2.0中,开始重构属性绑定的功能。我们添加了一些新的抽象类和一些全新的绑定API。在本篇文章中,我们会介绍其中一些新的类和接口,并介绍添加他们的原因,以及如何在自己的代码中如何使用他们。Property Sources如果你已经使用spring有一段时间,你应该对Environment比较熟悉了。这个接口继承了PropertyResolver,让你从一些PropertySource的实现解析属性。Spring Framework提供了一些常用的PropertySource,如系统属性,命令行属性,属性文件等。Spring Boot自动配置这些实现(比如加载application.properties)。Configuration Property Sources比起直接使用已存在的PropertySource实现类,Spring Boot2.0引入了新的ConfigurationPropertySource接口。我们引入这个新的接口来定义“relaxed binding”规则。该接口的主要API显得非常简单ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name);另外有个IterableConfigurationPropertySource变量实现了Iterable<ConfigurationPropertyNaame>,让你可以发现source包含的所有属性名称。你可以向下面这样将Environment传给ConfigurationPropertySources:Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(environment);我们同时提供了MapConfigurationPropertySource来帮你应付上面的场景。Configuration Property Names如果规则明确,实现"relaxed binding"会简单很多。一直使用一致的格式,而不需要去关系在source中的各种无规则的格式。ConfigurationPropertyNames类来强制进行这些属性命名规则,例如“use lowercase kebab-case names”,在代码中使用person.first-name,在source中使用person.firstName或者PERSON_FIRSTNAME.Origin Support如期望的那样,ConfigurationPropertySource返回ConfigurationProperty对象,里面包含了属性的取值,另外有个可选的Origin对象。spring boot 2.0引入了新的接口Origin,能够指出属性取值的准确位置。其中TextResourceOrigin是较为常用的实现,会提供所加载的Resource,以及对应的行。对于.properties和.yml文件,我们写了定制的souce加载器,使得追踪成为可能。一些spring boot的功能进行了重写来追踪信息。比如,属性绑定的验证异常现在会显示:APPLICATION FAILED TO STARTDescription:Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under ‘person’ to scratch.PersonProperties failed: Property: person.name Value: Joe Origin: class path resource [application.properties]:1:13 Reason: length must be between 4 and 2147483647Action:Update your application’s configurationBinder APIorg.springframework.boot.context.properties.bind.Binder类允许你使用多个ConfigurationPropertySource。准确的说,Binder使用Bindable并返回一个BindResult.Bindable一个Bindable可以是Java bean,或是一个复杂对象(如List<Person>).这里是一些例子Bindable.ofInstance(existingBean);Bindable.of(Integer.class);Bindable.listOf(Person.class);Bindable.of(resovableType);Binable用来携带注解的信息,但不需要太过关注这个。BindResultbinder会返回BindResult,和java8 stream操作返回的Optional很相似,一个BinderResult表示bind的结果。如果你尝试获取一个没有绑定的对象,会抛出异常,另外有其他的方法来设置没有绑定时的缺省对象。var bound = binder.bind(“person.date-of-birth”,Bindable.of(LocalDate.class));//返回LocalDate,如果没有则抛出异常bound.get()//返回一个格式化时间,或是“No DOB"bound.map(dateFormatter::format).orElse(“NO DOB”);//返回LocalDate或者抛出自定义异常bound.orElseThrow(NoDateOfBirthException::new);Formatting and Conversion大部分ConfigurationPropertySource实现将值当字符串处理。当Binder需要将值转化为其他类型时,会使用到Spring的ConversionService API。比较常用的是@NumberFormat和@DateFormat这两个注解。spring boot 2.0也引入了一些新的注解和转换器。例如,你现在可以将4s转换成Duration。具体请参考org.springframework.boot.conver包。BindHandler在binding的过程中,你可能会需要实现一些额外的逻辑。BindHandler接口提供了这样的机会。每一个BindHandler有onStart, onSuccess, onFailure和 onFinish方法供重载。spring boot提供了一些handlers,用来支持已存在的@ConfigurationProperties binding。 比如 ValidationBindHandler可以用于绑定对象的合法性校验上。@ConfigurationProperties如文章开头所提到的,@ConfigurationProperties是在spring boot最初就加入的功能。所以大部分人会继续使用该注解是不可避免的。Future Work我们计划在spring boot2.1中继续加强Binder的功能,而第一个想要支持的功能是不可变属性绑定。另外相较getters和setters的绑定,使用基于构造器的绑定来代替:public class Person{ private final String firstName; private final String lastName; private final LocalDateTime dateOfBirth; public Person(String firstName, String lastName, LocalDateTime dateOfBirth){ this.firstName = firstName; this.lastName = lastName; this.dateOfBirth = dateOfBirth; } //getters}Summary我们希望在spring boot2.0 中你可以找到更好用的属性绑定功能,并考虑升级你目前的方式。 ...

April 5, 2019 · 1 min · jiezi

问题排查之RocketMQAutoConfiguration not loaded.

背景今天将一个SpringBoot项目的配置参数从原有的.yml文件迁移到Apollo后,启动报错Bean method ‘rocketMQTemplate’ in ‘RocketMQAutoConfiguration’ not loaded because @ConditionalOnBean (types: org.apache.rocketmq.client.producer.DefaultMQProducer; SearchStrategy: all) did not find any beans of type org.apache.rocketmq.client.producer.DefaultMQProducer花了两个小时才最终搞清楚,原因是缺少了配置项 spring.rocketmq.producer.group 从而导致无法成功创建RocketMQAutoConfiguration这个Bean,从而导致一连串对此有依赖的Bean无法创建成功。排查过程启动的错误日志2019-04-02 15:21:33.689 WARN 17516 — [ main] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘myServiceAImpl’: Unsatisfied dependency expressed through field ‘myServiceBImpl’; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘myServiceAImpl’: Unsatisfied dependency expressed through field ‘rocketMQTemplate’; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘org.apache.rocketmq.spring.starter.core.RocketMQTemplate’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}2019-04-02 15:21:33.692 INFO 17516 — [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} closed2019-04-02 15:21:33.693 INFO 17516 — [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService2019-04-02 15:21:33.693 INFO 17516 — [ main] f.a.ReferenceAnnotationBeanPostProcessor : class com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor was destroying!2019-04-02 15:21:33.702 INFO 17516 — [ main] ConditionEvaluationReportLoggingListener : Error starting ApplicationContext. To display the conditions report re-run your application with ‘debug’ enabled.2019-04-02 15:21:33.842 ERROR 17516 — [ main] o.s.b.d.LoggingFailureAnalysisReporter : APPLICATION FAILED TO STARTDescription:Field rocketMQTemplate in net.yourpackage.myServiceBImpl required a bean of type ‘org.apache.rocketmq.spring.starter.core.RocketMQTemplate’ that could not be found. - Bean method ‘rocketMQTemplate’ in ‘RocketMQAutoConfiguration’ not loaded because @ConditionalOnBean (types: org.apache.rocketmq.client.producer.DefaultMQProducer; SearchStrategy: all) did not find any beans of type org.apache.rocketmq.client.producer.DefaultMQProducerAction:Consider revisiting the conditions above or defining a bean of type ‘org.apache.rocketmq.spring.starter.core.RocketMQTemplate’ in your configuration.问题原因从上图中可以看到RocketMQAutoConfiguration中的mqProducer方法会根据配置参数来创建DefaultMQProducer,其中有两个必要的参数spring.rocketmq.nameServerspring.rocketmq.producer.group在重新检查了一遍配置文件后发现,的确是因为漏掉了spring.rocketmq.producer.group,将其加上之后便可以成功启动项目了。 ...

April 4, 2019 · 1 min · jiezi

使用maven创建简单的多模块 Spring Web项目

第一次写技术文章,主要内容是使用maven创建一个简单的SpringMVC WEB 项目,如有操作或理解错误请务必指出,当谦虚学习。做这一次的工作主要是因为想加强一下自己对Spring Web 项目的理解,因为平时都是直接写业务代码,我觉得还是有必要自己了解一下创建项目的过程。后续会基于这个项目写更多的SpringWeb开发过程,希望能帮助到有需要的人。总的来说是一个相当精简的多模块springWeb项目搭建过程,让我们进入正题吧我们知道单体应用,就写在一个project里面的话,业务一旦庞大起来非常难以管理。把各个模块单独抽出来可以方便的对jar包进行版本管理(尽管我还没经历过这个),维护项目,团队开发也会方便许多。基本思想其实就是一个java web项目引用别的模块Jar包,最终web项目被打成war包发布。而所有的war包项目,jar包项目都是在同一个父模块下管理的(它们都是Maven项目)(如果你有IDE,装好插件就用IDE创建吧,我个人不喜欢手动命令行创建)1. 创建父项目下图中:框起来打勾这个会让你跳过项目模式选择,勾选对于创建项目没有什么影响,以后也许会转一下Maven这方面的文章POM包才能做父项目,谨记!!!!! 2. 子项目结构和创建以下是我的结构分层,你也可以按你的想法来,最终目的是要方便自己开发。test_parent (父项目) |—-test_web (web项目) |—-test_service (业务内容) |—-test_framework (工具,框架封装、配置) |—-test_dao (数据持久层,DO也放这) |—-test_controller (处理映射) 创建子项目直接右键父项目然后新建maven module ,也就是子模块我们先创建web模块,这里你可以勾选第一条然后创建简单项目,如果没有勾选,那么你要在下一步里选择 maven-achetype-webapp,这里以简单项目为例子Group Id 和 version 都是继承父项目的一定要选择war包打包,不然要重新把他构建成web项目。如果你没选war包:https://www.cnblogs.com/leonk…最后点finish完成点击生成Web描述文件 (web.xml)这样就完成了Web模块的创建,剩下的其他项目都是同样的步骤创建,都是选择jar包,参考下图:3. 配置各模块的pom.xmlpom.xml记录所需的jar包,模块联系,包信息,打包参数等等信息,在多模块里我们要理清关系,不要重复引用首先毫无疑问的是让parent加载spring的jar包是最方便开发的,因为默认所有模块都继承parent,所以子模块引用spring内容也方便。其次配置文件我们统一放在framework中进行管理。那么先来写入web.xml配置吧<?xml version=“1.0” encoding=“UTF-8”?><web-app xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://java.sun.com/xml/ns/javaee" xsi:schemaLocation=“http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id=“WebApp_ID” version=“3.0”> <display-name>test_web</display-name> <context-param> <!– 配置地址 –> <param-name>contextConfigLocation</param-name> <param-value>classpath*:spring-.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <!– spring-mvc.xml 配置地址 –> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/</url-pattern> </filter-mapping></web-app>这里可以看到我们写了classpath,原因是它能搜索到项目目录以外的Jar包下文件相关:http://www.cnblogs.com/wlgqo/…web.xml详解:https://blog.csdn.net/qq_3557…web.xml是对WEB项目来说是必须的配置文件,写好了spring配置文件的位置以后,就来新建2个spring配置文件,新建的配置放在test_framework模块里,路径如下图 spring-context.xml spring-mvc.xml一个是spring-context.xml 也叫applicationContext.xml,是webApp的上下文配置,也可以理解为配置dao、service 通用bean的地方,但我们这里使用的是注解扫描方式配置bean,所以就简单许多,即便有工具存在,写改xml真的很讨厌啊!<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns=“http://www.springframework.org/schema/beans" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc=“http://www.springframework.org/schema/mvc" xmlns:context=“http://www.springframework.org/schema/context" xmlns:tx=“http://www.springframework.org/schema/tx" xmlns:util=“http://www.springframework.org/schema/util" xmlns:aop=“http://www.springframework.org/schema/aop" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd"> <!– 注解注册 –> <!– <context:annotation-config /> –> <context:component-scan base-package=“com.test” > <context:exclude-filter type=“annotation” expression=“org.springframework.stereotype.Controller” /> <context:exclude-filter type=“annotation” expression=“org.springframework.web.bind.annotation.RestController” /> </context:component-scan> </beans>这里要去掉对controller的扫描,applicationContext初始化的上下文加载的Bean是对于整个应用程序共享的,不管是使用什么表现层技术,一般如DAO层、Service层Bean; DispatcherServlet (下一个要配置的东西) 初始化的上下文加载的Bean是只对Spring Web MVC有效的Bean,如Controller、HandlerMapping、HandlerAdapter等等,该初始化上下文应该只加载Web相关组件。context:component-scan 的 base-package 值用来决定我们需要扫描的包的基础名,具体配置相关可以看:https://www.cnblogs.com/exe19…而context:annotation-config/ 呢?其实也是spring为了方便我们开发者给我们提供的一个自动识别注解的配置,相关细节如下:解释说明:https://www.cnblogs.com/_popc…两条配置的区别和诠释:https://www.cnblogs.com/leiOO…下面是第二个配置文件 spring-mvc.xml<?xml version=“1.0” encoding=“UTF-8” standalone=“no”?><beans xmlns=“http://www.springframework.org/schema/beans" xmlns:context=“http://www.springframework.org/schema/context" xmlns:aop=“http://www.springframework.org/schema/aop" xmlns:mvc=“http://www.springframework.org/schema/mvc" xmlns:p=“http://www.springframework.org/schema/p" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd"> <!– 自动扫描的包名 –> <context:component-scan base-package=“com.test.*.controller” > <context:include-filter type=“annotation” expression=“org.springframework.stereotype.Controller” /> </context:component-scan> <!– 默认的注解映射的支持 –> <mvc:annotation-driven> <mvc:message-converters> <bean class=“org.springframework.http.converter.StringHttpMessageConverter”> <constructor-arg value=“UTF-8” /> </bean> <bean class=“org.springframework.http.converter.ResourceHttpMessageConverter” /> </mvc:message-converters> </mvc:annotation-driven></beans>包扫描没什么好说的,这里还强调include了注解controller。mvc:annotation-driven 是spring默认的注解驱动,这个配置项一口气把一堆东西都给我们加进来了,但主要还是针对controller和处理请求的,具体的在下面文章中,因为加的内容有点多,所以这个留到后面研究,稍微理解作用就好:相关文章 : https://blog.csdn.net/vicroad...mvc:message-converters 顾名思义,就是用于处理请求消息的,request content-header 会记录请求的内容类型,根据这些类型,spring会把内容转化成服务器操作的对象,这里的字符串转化是为了避免乱码,我们指定了编码格式。相关文章:https://www.jianshu.com/p/2f6…以上,我们就已经把最简约的配置写好了。接下来我们随便写一个controller试试4.写个Controller吧根据之前写好的controller的扫描包名,去我们test_controller模块里创建一个controllerpackage com.test.hello.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/test”)public class HelloController{ @RequestMapping("/get”) public String helloGet(@RequestParam String str) throws Exception { return str; }}很简单的一段返回请求字符串的代码,现在一切就绪可以启动服务器了,配置好Tomcat就可以启动了,右键test_web –> run as –> run on server 选择创建好的tomcat容器,就可以启动了。接下来,访问: localhost:8080/test/test/get?str=helloWorld 如果你使用eclipse启动且没有正常启动,特别是出现严重错误时,请先检查web.xml配置命名有没有问题,然后再检查test_web项目的assembly,这个会影响项目的发布文件,下图所示,右键项目点properties,没有test_framework的话就加入framework项目。网站无响应,检查一下tomcat的端口,默认是8080。404检查代码的映射路径。5.结束语第一次写技术文章,记录一下自己的学习过程,日后会以当前项目作为基础,继续记录下自己遇到的问题和分享的知识,希望能帮助到一部分新手,此外,本篇文章中若有错误,欢迎指出,我会不断更新文章误点,不吝赐教。 ...

April 4, 2019 · 2 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

ApiBoot - ApiBoot Alibaba Sms 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。ApiBoot的短信服务模块是由阿里云的国际短信服务提供的,支持国内和国际快速发送验证码、短信通知和推广短信。前提:需要到阿里云控制台申请开通短信服务。引入ApiBoot Alibaba Sms在pom.xml配置文件内添加如下:<!–ApiBoot Alibaba Sms–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-alibaba-sms</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,具体查看ApiBoot版本依赖配置参数列表配置参数参数介绍默认值是否必填api.boot.sms.access-key-idRAM账号的AccessKey ID空是api.boot.sms.access-key-secretRAM账号Access Key Secret空是api.boot.sms.sign-name短信签名空是api.boot.sms.connection-timeout短信发送连接超时时长10000否api.boot.sms.read-timeout短信接收消息连接超时时长10000否api.boot.sms.profile短信区域环境default否发送短信在ApiBoot Alibaba Sms模块内置了ApiBootSmsService接口实现类,通过send方法即可完成短信发送,如下所示: /** * logger instance */ static Logger logger = LoggerFactory.getLogger(ApiBootSmsTest.class); @Autowired private ApiBootSmsService apiBootSmsService; @Test public void sendSms() { // 参数 ApiBootSmsRequestParam param = new ApiBootSmsRequestParam(); param.put(“code”, “192369”); // 请求对象 ApiBootSmsRequest request = ApiBootSmsRequest.builder().phone(“171xxxxx”).templateCode(“SMS_150761253”).param(param).build(); // 发送短信 ApiBootSmsResponse response = apiBootSmsService.send(request); logger.info(“短信发送反馈,是否成功:{}”, response.isSuccess()); }短信模板code自行从阿里云控制台获取。如果在阿里云控制台定义的短信模板存在多个参数,可以通过ApiBootSmsRequestParam#put方法来进行挨个添加,该方法返回值为ApiBootSmsRequestParam本对象。多参数多参数调用如下所示:// 参数ApiBootSmsRequestParam param = new ApiBootSmsRequestParam();param.put(“code”, “192369”).put(“name”, “测试名称”);发送结果反馈执行短信发送后会返回ApiBootSmsResponse实例,通过该实例即可判断短信是否发送成功。本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-alibaba-sms

April 4, 2019 · 1 min · jiezi

ApiBoot - ApiBoot Quartz 使用文档

ApiBoot QuartzApiBoot内部集成了Quartz,提供了数据库方式、内存方式的进行任务的存储,其中数据库方式提供了分布式集群任务调度,任务自动平滑切换执行节点。引用ApiBoot Quartz在pom.xml配置文件内添加,如下配置:<!–ApiBoot Quartz–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-quartz</artifactId></dependency>备注:如果使用ApiBoot Quartz的内存方式,仅需要添加上面的依赖即可。相关配置参数名称是否必填默认值描述api.boot.quartz.job-store-type否memory任务存储源方式,默认内存方式api.boot.quartz.scheduler-name否scheduler调度器名称api.boot.quartz.auto-startup否true初始化后是否自动启动调度程序api.boot.quartz.startup-delay否0初始化完成后启动调度程序的延迟。api.boot.quartz.wait-for-jobs-to-complete-on-shutdown否false是否等待正在运行的作业在关闭时完成。api.boot.quartz.overwrite-existing-jobs否false配置的作业是否应覆盖现有的作业定义。api.boot.quartz.properties否 Quartz自定义的配置属性,具体参考quartz配置api.boot.quartz.jdbc否 配置数据库方式的Jdbc相关配置内存方式ApiBoot Quartz在使用内存方式存储任务时,不需要做配置调整。数据库方式需要在application.yml配置文件内修改api.boot.quartz.job-store-type参数,如下所示:api: boot: quartz: # Jdbc方式 job-store-type: jdbcQuartz所需表结构Quartz的数据库方式内部通过DataSource获取数据库连接对象来进行操作数据,所操作数据表的表结构是固定的,ApiBoot把Quartz所支持的所有表结构都进行了整理,访问Quartz支持数据库建表语句列表查看,复制执行对应数据库语句即可。创建任务类我们只需要让新建类集成QuartzJobBean就可以完成创建一个任务类,如下简单示例:/** * 任务定义示例 * 与Quartz使用方法一致,ApiBoot只是在原生基础上进行扩展,不影响原生使用 * <p> * 继承QuartzJobBean抽象类后会在项目启动时会自动加入Spring IOC * * @author:恒宇少年 - 于起宇 * <p> * DateTime:2019-03-28 17:26 * Blog:http://blog.yuqiyu.com * WebSite:http://www.jianshu.com/u/092df3f77bca * Gitee:https://gitee.com/hengboy * GitHub:https://github.com/hengboy /public class DemoJob extends QuartzJobBean { /* * logger instance */ static Logger logger = LoggerFactory.getLogger(DemoJob.class); @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { logger.info(“定时任务Job Key : {}”, context.getJobDetail().getKey()); logger.info(“定时任务执行时所携带的参数:{}”, JSON.toJSONString(context.getJobDetail().getJobDataMap())); //…处理逻辑 }}任务参数在任务执行时传递参数是必须的,ApiBoot Quartz提供了比较方便的传递方式,不过最终Quartz会把传递的值都会转换为String类型数据。任务Key默认值ApiBoot Quartz的newJob方法所创建的定时任务,如果在不传递Job Key参数时,会默认使用UUID随机字符串作为Job Key以及Trigger Key。自定义任务开始时间任务开始时间可以通过startAtTime方法进行设置,在不设置的情况下,任务创建完成后会立刻执行。Cron 表达式任务创建Cron类型任务如下所示:String jobKey = apiBootQuartzService.newJob(ApiBootCronJobWrapper.Context() .jobClass(DemoJob.class) .cron(“0/5 * * * * ?”) .param( ApiBootJobParamWrapper.wrapper().put(“param”, “测试”)) .wrapper());Cron 表达式任务由ApiBootCronJobWrapper类进行构建。上面的DemoJob任务类将会每隔5秒执行一次。Loop 重复任务Loop循环任务,当在不传递重复执行次数时,不进行重复执行,仅仅执行一次,如下所示:String jobKey = apiBootQuartzService.newJob( ApiBootLoopJobWrapper.Context() // 参数 .param( ApiBootJobParamWrapper.wrapper() .put(“userName”, “恒宇少年”) .put(“userAge”, 24) ) // 每次循环的间隔时间,单位:毫秒 .loopIntervalTime(2000) // 循环次数 .repeatTimes(5) // 开始时间,10秒后执行 .startAtTime(new Date(System.currentTimeMillis() + 10000)) // 任务类 .jobClass(DemoJob.class) .wrapper() );Loop 任务由ApiBootLoopJobWrapper类进行构建。上面的定时任务将会重复执行5次,连上自身执行的一次也就是会执行6次,每次的间隔时间为2秒,在任务创建10秒后进行执行。Once 一次性任务Once一次性任务,任务执行一次会就会被自动释放,如下所示:Map paramMap = new HashMap(1);paramMap.put(“paramKey”, “参数值”);String jobKey = apiBootQuartzService.newJob( ApiBootOnceJobWrapper.Context() .jobClass(DemoJob.class) // 参数 .param( ApiBootJobParamWrapper.wrapper() .put(“mapJson”, JSON.toJSONString(paramMap)) ) // 开始时间,2秒后执行 .startAtTime(new Date(System.currentTimeMillis() + 2000)) .wrapper());Once 任务由ApiBootOnceJobWrapper类进行构建。在参数传递时可以是对象、集合,不过需要进行转换成字符串才可以进行使用。暂停任务执行任务在执行过程中可以进行暂停操作,通过ApiBoot Quartz提供的pauseJob方法就可以很简单的实现,当然暂停时需要传递Job Key,Job Key可以从创建任务方法返回值获得。暂停任务如下所示:// 暂停指定Job Key的任务apiBootQuartzService.pauseJob(jobKey);// 暂停多个执行中任务apiBootQuartzService.pauseJobs(jobKey,jobKey,jobKey);恢复任务执行任务执行完暂停后,如果想要恢复可以使用如下方式:// 恢复指定Job Key的任务执行apiBootQuartzService.resumeJob(jobKey);// 恢复多个暂停任务apiBootQuartzService.resumeJobs(jobKey,jobKey,jobKey);修改Cron表达式修改Cron表达式的场景如下:已创建 & 未执行已创建 & 已执行修改方法如下所示:// 修改执行Job Key任务的Cron表达式apiBootQuartzService.updateJobCron(jobKey, “0/5 * * * * ?”);删除任务想要手动释放任务时可以使用如下方式:// 手动删除指定Job Key任务apiBootQuartzService.deleteJob(jobKey);// 手动删除多个任务apiBootQuartzService.deleteJobs(jobKey,jobKey,jobKey);删除任务的顺序如下:暂停触发器移除触发器删除任务本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-quartz ...

April 4, 2019 · 1 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

微服务五种开源API网关实现组件对比

作者:康学志 from博云研究院微服务架构是当下比较流行的一种架构风格,它是一种以业务功能组织的服务集合,可以持续交付、快速部署、更好的可扩展性和容错能力,而且还使组织更容易去尝试新技术栈。微服务具有几个关键特征:·高度可维护和可测试性·与其他服务松散耦合·且可独立部署·能够由一个小团队开发现在很多公司企业想将自己的单体应用架构迁移到微服务架构,在这个问题上,Martin Fowler提出了3个前提,而Phil Calcado对其进行了扩展如下:·快速配置计算资源·基本监控·快速部署·易于配置存储·易于访问网关·认证、授权·标准化的 RPC今天我们主要讲讲易于访问的网关,也就是 API 网关。为什么需要 API 网关假设我们要使用微服务架构构建一个电商平台,以下可能是一些潜在的服务:·购物车服务·订单服务·商品服务·评论服务·库存服务等等,客户端应该怎么访问这些服务呢?原来单体应用的情况我们都知道,都在一个服务器上部署,直接访问IP+端口+服务前缀即可,现在微服务架构下,每个服务都可以独立部署,并且是由不同的开发团队开发的,难道我们要这样访问吗?理论上每个服务都有端点可以访问,但是客户端就需要记录所有服务的端点,发起5次请求,现在还是5个服务,如果之后扩展多了呢?对客户端来说就是一个灾难,随之带来的就是安全性问题、扩展性问题等,所以这种客户端直接与每个服务交互是不可取的,通常,更好的方式是使用 API 网关。API 网关是客户端访问服务的统一入口,API 网关封装了后端服务,还提供了一些更高级的功能,例如:身份验证、监控、负载均衡、缓存、多协议支持、限流、熔断等等,API 网关还可以针对不同的客户端定制不同粒度的 API,上面例子中修改架构后如下:API 网关的优缺点API 网关的好处是显而易见的,封装了应用程序的内部结构,为不同客户端提供不同粒度的 API,同时网关自身也提供了一些高级功能,也减少了客户端与应用程序之间的往返次数,使客户端代码更优雅。同时使用网关也存在一些缺点,增加了一个新的组件,增加了整个应用架构的复杂度,一个通俗的道理,你做的越多你犯错的风险也越高,网关不可用很可能导致整个应用架构崩溃,当然现在有各种各样的方案,能防止网关崩溃,它也可能存在瓶颈风险。使用网关有利有弊,我个人而言,利肯定是大于弊的,我们尽可能的将弊端降到最低。API 网关一些实现使用一个组件时,尤其是这种比较流行的架构,组件肯定存在开源的,我们不必自己去从零开始去实现一个网关,自己开发一个网关的工作量是相当可观的,现在比较流行的开源 API 网关如下所示:KongKong是一个在 Nginx 中运行的Lua应用程序,并且可以通过lua-nginx模块实现,Kong不是用这个模块编译Nginx,而是与 OpenResty 一起发布,OpenResty已经包含了 lua-nginx-module, OpenResty 不是 Nginx 的分支,而是一组扩展其功能的模块。它的核心是实现数据库抽象,路由和插件管理,插件可以存在于单独的代码库中,并且可以在几行代码中注入到请求生命周期的任何位置。TraefikTraefik 是一个现代 HTTP 反向代理和负载均衡器,可以轻松部署微服务,Traeffik 可以与您现有的组件(Docker、Swarm,Kubernetes,Marathon,Consul,Etcd,…)集成,并自动动态配置。AmbassadorAmbassador 是一个开源的微服务 API 网关,建立在 Envoy 代理之上,为用户的多个团队快速发布,监控和更新提供支持,支持处理 Kubernetes ingress controller 和负载均衡等功能,可以与 Istio 无缝集成。TykTyk是一个开源的、轻量级的、快速可伸缩的 API 网关,支持配额和速度限制,支持认证和数据分析,支持多用户多组织,提供全 RESTful API。基于 go 编写。ZuulZuul 是一种提供动态路由、监视、弹性、安全性等功能的边缘服务。Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器。API 网关实现对比总结由上述对比表格中可以看出:从开源社区活跃度来看,无疑是Kong和Traefik较好;从成熟度来看,较好的是Kong、Tyk、Traefik;从性能角度来看,Kong要比其他几个领先一些;从架构优势的扩展性来看,Kong、Tyk有丰富的插件,Ambassador也有插件但不多,而Zuul是完全需要自研,但Zuul由于与Spring Cloud深度集成,使用度也很高,近年来Istio服务网格的流行,Ambassador因为能够和Istio无缝集成也是相当大的优势。具体使用选择还是需要依据具体的业务场景,我们在参考链接中收集了一些性能对比,大家可以做下参考。参考链接https://www.bbva.com/en/api-g… kong vs tykhttps://stackshare.io/stackup… kong vs traefikhttps://blog.getambassador.io… envoy vs nginxhttps://engineering.opsgenie…. nginx vs zuul ...

April 4, 2019 · 1 min · jiezi

SpringBoot | 自动配置原理

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言这个月过去两天了,这篇文章才跟大家见面,最近比较累,大家见谅下。下班后闲着无聊看了下 SpringBoot 中的自动配置,把我的理解跟大家说下。配置文件能写什么?相信接触过 SpringBoot 的朋友都知道 SpringBoot 有各种 starter 依赖,想要什么直接勾选加进来就可以了。想要自定义的时候就直接在配置文件写自己的配置就好。但你们有没有困惑,为什么 SpringBoot 如此智能,到底配置文件里面能写什么呢?带着这个疑问,我翻了下 SpringBoot 官网看到这么一些配置样例:发现 SpringBoot 可配置的东西非常多,上图只是节选。有兴趣的查看这个网址:https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#boot-features-external-config-yaml自动配置原理这里我拿之前创建过的 SpringBoot 来举例讲解 SpringBoot 的自动配置原理,首先看这么一段代码:@SpringBootApplicationpublic class JpaApplication { public static void main(String[] args) { SpringApplication.run(JpaApplication.class, args); }}毫无疑问这里只有 @SpringBootApplication 值得研究,进入 @SpringBootApplication 的源码:SpringBoot 启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration,再进入 @EnableAutoConfiguration 源码:发现最重要的就是 @Import(AutoConfigurationImportSelector.class) 这个注解,其中的 AutoConfigurationImportSelector 类的作用就是往 Spring 容器中导入组件,我们再进入这个类的源码,发现有这几个方法:/*** 方法用于给容器中导入组件**/@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader .loadMetadata(this.beanClassLoader); AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry( autoConfigurationMetadata, annotationMetadata); // 获取自动配置项 return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}// 获取自动配置项protected AutoConfigurationEntry getAutoConfigurationEntry( AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } AnnotationAttributes attributes = getAttributes(annotationMetadata); List < String > configurations = getCandidateConfigurations(annotationMetadata, attributes); // 获取一个自动配置 List ,这个 List 就包含了所有自动配置的类名 configurations = removeDuplicates(configurations); Set < String > exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = filter(configurations, autoConfigurationMetadata); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions);}// 获取一个自动配置 List ,这个 List 就包含了所有的自动配置的类名protected List < String > getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 通过 getSpringFactoriesLoaderFactoryClass 获取默认的 EnableAutoConfiguration.class 类名,传入 loadFactoryNames 方法 List < String > configurations = SpringFactoriesLoader.loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, “No auto configuration classes found in META-INF/spring.factories. If you " + “are using a custom packaging, make sure that file is correct.”); return configurations;}// 默认的 EnableAutoConfiguration.class 类名protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class;}代码注释很清楚:首先注意到 selectImports 方法,其实从方法名就能看出,这个方法用于给容器中导入组件,然后跳到 getAutoConfigurationEntry 方法就是用于获取自动配置项的。再来进入 getCandidateConfigurations 方法就是 获取一个自动配置 List ,这个 List 就包含了所有的自动配置的类名 。再进入 SpringFactoriesLoader 类的 loadFactoryNames 方法,跳转到 loadSpringFactories 方法发现 ClassLoader 类加载器指定了一个 FACTORIES_RESOURCE_LOCATION 常量。然后利用PropertiesLoaderUtils 把 ClassLoader 扫描到的这些文件的内容包装成 properties 对象,从 properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中。public static List < String > loadFactoryNames(Class < ? > factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());}private static Map < String, List < String >> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap < String, String > result = cache.get(classLoader); if (result != null) { return result; } try { // 扫描所有 jar 包类路径下 META-INF/spring.factories Enumeration < URL > urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap < > (); while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); // 把扫描到的这些文件的内容包装成 properties 对象 Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry < ? , ? > entry : properties.entrySet()) { String factoryClassName = ((String) entry.getKey()).trim(); for (String factoryName: StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { // 从 properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中 result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException(“Unable to load factories from location [” + FACTORIES_RESOURCE_LOCATION + “]”, ex); }}点击 FACTORIES_RESOURCE_LOCATION 常量,我发现它指定的是 jar 包类路径下 META-INF/spring.factories 文件:public static final String FACTORIES_RESOURCE_LOCATION = “META-INF/spring.factories”;将类路径下 META-INF/spring.factories 里面配置的所有 EnableAutoConfiguration 的值加入到了容器中,所有的 EnableAutoConfiguration 如下所示:注意到 EnableAutoConfiguration 有一个 = 号,= 号后面那一串就是这个项目需要用到的自动配置类。# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherProviderAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration每一个这样的 xxxAutoConfiguration 类都是容器中的一个组件,都加入到容器中,用他们来做自动配置。上述的每一个自动配置类都有自动配置功能,也可在配置文件中自定义配置。举例说明 Http 编码自动配置原理@Configuration // 表示这是一个配置类,以前编写的配置文件一样,也可以给容器中添加组件@EnableConfigurationProperties(HttpEncodingProperties.class) // 启动指定类的 ConfigurationProperties 功能;将配置文件中对应的值和 HttpEncodingProperties 绑定起来;并把 HttpEncodingProperties 加入到 ioc 容器中@ConditionalOnWebApplication // Spring 底层 @Conditional 注解,根据不同的条件,如果满足指定的条件,整个配置类里面的配置就会生效;判断当前应用是否是 web 应用,如果是,当前配置类生效@ConditionalOnClass(CharacterEncodingFilter.class) // 判断当前项目有没有这个类 CharacterEncodingFilter;SpringMVC 中进行乱码解决的过滤器;@ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”, matchIfMissing = true) // 判断配置文件中是否存在某个配置 spring.http.encoding.enabled;如果不存在,判断也是成立的// 即使我们配置文件中不配置 pring.http.encoding.enabled=true,也是默认生效的;public class HttpEncodingAutoConfiguration { // 已经和 SpringBoot 的配置文件建立映射关系了 private final HttpEncodingProperties properties; //只有一个有参构造器的情况下,参数的值就会从容器中拿 public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) { this.properties = properties; } @Bean // 给容器中添加一个组件,这个组件的某些值需要从 properties 中获取 @ConditionalOnMissingBean(CharacterEncodingFilter.class) public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE)); return filter; }自动配置类做了什么不在这里赘述,请见上面代码。所有在配置文件中能配置的属性都是在 xxxxProperties 类中封装的;配置文件能配置什么就可以参照某个功能对应的这个属性类,例如上述提到的 @EnableConfigurationProperties(HttpProperties.class) ,我们打开 HttpProperties 文件源码节选:@ConfigurationProperties(prefix = “spring.http”)public class HttpProperties { public static class Encoding { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; /** * Charset of HTTP requests and responses. Added to the “Content-Type” header if * not set explicitly. / private Charset charset = DEFAULT_CHARSET; /* * Whether to force the encoding to the configured charset on HTTP requests and * responses. */ private Boolean force;}在上面可以发现里面的属性 charset 、force 等,都是我们可以在配置文件中指定的,它的前缀就是 spring.http.encoding 如:另外,如果配置文件中有配该属性就取配置文件的,若无就使用 XxxxProperties.class 文件的默认值,比如上述代码的 Charset 属性,如果不配那就使用 UTF-8 默认值。总结1. SpringBoot 启动会加载大量的自动配置类2. 我们看我们需要的功能有没有 SpringBoot 默认写好的自动配置类;3. 我们再来看这个自动配置类中到底配置了哪些组件 ( 只要我们要用的组件有,我们就不需要再来配置,若没有,我们可能就要考虑自己写一个配置类让 SpringBoot 扫描了)4. 给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性。我们就可以在配置文件中指定这些属性的值;xxxxAutoConfigurartion 自动配置类的作用就是给容器中添加组件xxxxProperties 的作用就是封装配置文件中相关属性至此,总算弄明白了 SpringBoot 的自动配置原理。我水平优先,如有不当之处,敬请指出,相互交流学习,希望对你们有帮助。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

April 4, 2019 · 3 min · jiezi

手动实现“低配版”IOC

引言IOC,全称Inversion of Control,控制反转。也算是老生常谈了。老生常谈:原指老书生的平凡议论;今指常讲的没有新意的老话。同时另一个话题,依赖注入,用什么就声明什么,直接就声明,或者构造函数或者加注解,控制反转是实现依赖注入的一种方式。通过依赖注入:我们无需管理对象的创建,通过控制反转:我们可以一键修改注入的对象。最近在做Android实验与小程序相关的开发,发现用惯了IOC的我们再去手动new对象的时候总感觉心里不舒服,以后改起来怎么办呢?既然没有IOC,我们就自己写一个吧。实现就像我在标题中描述的一样,我先给大家讲解一下标配IOC的原理,使大家更清晰明了,但是受Android与小程序相关的限制,我具体的实现,是低配版IOC。个人扯淡不管是Spring还是Angular,要么是开源大家,要么是商业巨头,具体的框架实现都是相当优秀。我还没水平也没精力去研读源码,只希望分享自己对IOC的理解,帮到更多的人。毕竟现在小学生都开始写Python,以后的框架设计会越来越优秀,学习成本越来越低,开发效率越来越高。可能是个人报个培训班学个俩月也会设计微服务,也能成为全栈工程师。所以我们应该想的是如何设计框架,而不是仅停留在使用的层面,渐渐地被只会写增删改查天天搬砖的人取代。996加班的工程师都开始挤时间写框架扩大影响力,让社会听到程序员的呐喊,我们还有什么不努力的理由?“标配”IOC不管是Spring还是Angular,它们的核心是什么呢?打上mvn spring-boot:run后台就Started Application in xxx seconds了,它到底干什么了呢?容器Spring与Angular就是一个大的IOC容器,所以应用启动的过程,其实就是构造容器的过程。容器,肯定是装东西的啊?IOC容器里装的是什么?装的是对象。控制器Controller,服务Service,自定义的组件Component,所有被Spring管理的对象都将被放进IOC容器里。所以,大家应该能明白,为什么IOC是依赖注入的一种实现方式?因为这个对象不是你自己new的,是从容器中拿的,容器初始化的时候,就已经把这个对象构造好了,该注的都注进来了。思考从上面大家可以看到,依赖注入的前提是什么?是要求这个对象必须是从容器中拿的,所以才能依赖注入成功。Spring Boot中没问题,Tomcat转发的路由直接交给容器中相应的对象去处理,同理,Angular也一样。Android呢?手机调用的并不是我们构造的Activity,而是它自己实例化的,小程序也与之类似,Page的实例化不归我们管。所以“标配”IOC在这里不适用,所以我设计了“低配”IOC容器。“低配”IOC找点自己能管得了的对象放在IOC容器里,这样再需要对象就不用去new了,Service有变更直接修改注入就行了。受我们管理的只有Service,计划设计一个管理所有Service的IOC容器,然后Activity或Page里用的时候,直接从容器中拿。(低配在这里,不能依赖注入了,得自己拿)。Android端一个单例的Configuration负责注册Bean和获取Bean,存储着一个context上下文。看着挺高级的,其实就是一个HashMap存储着接口类型到容器对象的映射。/** * 全局配置类 /public class Configuration { private static Map<Class<?>, Object> context = new HashMap<>(); private static final class Holder { private static final Configuration INSTANCE = new Configuration(); } public static Configuration getInstance() { return Holder.INSTANCE; } public Configuration registerBean(Class<?> clazz, Object bean) { context.put(clazz, bean); return this; } public <T> T getBean(Class<?> clazz) { return (T) context.get(clazz); }}写一个静态方法,更加方便配置。/* * 云智,全局配置辅助类 */public class Yunzhi { … public static <T> T getBean(Class<?> clazz) { return Configuration.getInstance().getBean(clazz); }}一个Application负责容器中所有对象的创建。public class App extends Application { @Override public void onCreate() { super.onCreate(); Yunzhi.init() .setApi(“http://192.168.2.110:8888”) .setTimeout(1L) .registerBean(AuthService.class, new AuthServiceImpl()) .registerBean(LetterService.class, new LetterServiceImpl()); }}使用方法和原来就一样了,一个接口,一个实现类。这里用到了RxJava,看上去倒是类似我们的Angular了。public interface AuthService { Observable<Auth> login(String username, String password);}public class AuthServiceImpl implements AuthService { private static final String TAG = “AuthServiceImpl”; @Override public Observable<Auth> login(String username, String password) { Log.d(TAG, “BASIC 认证”); String credentials = username + “:” + password; String basicAuth = “Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP); Log.d(TAG, “请求登录”); return HttpClient.request(AuthRequest.class) .login(basicAuth) .subscribeOn(Schedulers.io()) // 在IO线程发起网络请求 .observeOn(AndroidSchedulers.mainThread()); // 在主线程处理 }}因为Activity我们管不着,所以在Activity里用不了依赖注入,需要手动从容器里拿。@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.authService = Yunzhi.getBean(AuthService.class);}这里有一处没考虑到的问题,就是手机端的性能问题,手机和服务器的处理能力肯定是比不了的,服务器在初始化的时候把所有对象都创建处理无可厚非,但是感觉手机端这样做还是会对性能产生一定影响的。应该是某些对象用到的时候再创建,实验要求时间很紧,这个就先这样吧,不改了。小程序端小程序是在Android之后写的,想到了之前设计的部分缺陷,对容器使用了另一种思想进行实现。export class YunzhiService { private static context = new Map<string, object>(); public static getBean(beanName: string): object { // 从context里拿对象 let bean = this.context.get(beanName); // 如果没有,构造一个,并放进context if (!bean) { bean = this.createBeanByName(beanName); this.context.set(beanName, bean); } // 返回 return bean; } public static createBeanByName(beanName: string): object { // 根据不同名称构造不同的Bean switch (beanName) { case Bean.AUTH_SERVICE: return new AuthServiceImpl(); case Bean.SCORE_SERVICE: return new ScoreServiceImpl(); case Bean.SEMESTER_SERVICE: return new SemesterServiceImpl(); default: throw ‘错误,未注册的bean’; } }}总结Spring Boot真厉害,什么时候我们也能写出如此优秀的框架? ...

April 3, 2019 · 2 min · jiezi

SpringBoot引入Thymeleaf

1.Thymeleaf简介 Thymeleaf是个XML/XHTML/HTML5模板引擎,可以用于Web与非Web应用 Thymeleaf的主要目标在于提供一种可被浏览器正确显示的、格式良好的模板创建方式,因此也可以用作静态建模,Thymeleaf的可扩展性也非常棒。你可以使用它定义自己的模板属性集合,这样就可以计算自定义表达式并使用自定义逻辑,Thymeleaf还可以作为模板引擎框架。2.引入Thymeleaf引入依赖在maven(pom.xml)中直接引入:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>配置Thymeleaf在application.yml配置Thymeleafserver: port: 8000spring: thymeleaf: cache: false # 关闭页面缓存 encoding: UTF-8 # 模板编码 prefix: classpath:/templates/ # 页面映射路径 suffix: .html # 试图后的后缀 mode: HTML5 # 模板模式# 其他具体配置可参考org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties# 上面的配置实际上就是注入该类的属性值demo示例创建IndexController@Controllerpublic class IndexController { // 返回视图页面 @RequestMapping(“index”) public String index(){ return “index”; }} 创建index.html<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Title</title></head><body> Hello Thymeleaf!</body></html> 创建TestController@RestControllerpublic class TestController { // 返回整个页面 @RequestMapping("/test") public ModelAndView test(){ return new ModelAndView(“test”); }} 创建test.html<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Title</title></head><body>Hello Thymeleaf! </br>By: ModelAndView</body></html>3.测试结果4.Thymeleaf基础语法及使用1.引入标签 html标签里引入xmlns:th=“http://www.thymeleaf.org"才能使用th:这样的语法 2.引入URL @{…} 例如:<a th:href=”@{http://www.baidu.com}">绝对路径</a> 是访问绝对路径下的URL, <a th:href="@{/}">相对路径</a> 是访问相对路径下的URL。<a th:href="@{css/bootstrap.min.css}">是引入默认的static下的css文件夹下的bootstrap文件,类似的标签有: th:href 和 th:src 3.获取变量 通过${}取值,对于JavaBean的话,使用变量名.属性名获取 4.字符串替换<span th:text="‘Welcome to our application, ’ + ${user.name} + ‘!’"></span>或者<span th:text="|Welcome to our application, ${user.name}!|"></span>注意:|…|中只能包含变量表达式${…},不能包含其他常量、条件表达式等5.运算符 在表达式中可以使用各类算术运算符 例如 (+, -, , /, %) 例如:th:with=“isEven=(${stat.number} % 1 == 0)” 逻辑运算符 (>, <, <=,>=,==,!=) 需要注意的是使用<,>的时候需要转义th:if="${stat.number} &gt; 1"th:text="‘Execution mode is ’ + ( (${execMode} == ‘dev’)? ‘Development’ : ‘Production’)“6.条件 if/unless th:if是该标签在满足条件的时候才会显示,unless是不成立时候才显示<a th:href=”@{/login}" th:unless=${user.number != null}>Login</a> switch thymeleaf支持switch结构,默认属性(default)用表示<div th:switch="${user.role}"> <p th:case="‘admin’">User is an administrator</p> <p th:case="#{roles.manager}">User is a manager</p> <p th:case="">User is some other thing</p></div>7.循环<tr th:each=“prod : ${prods}"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td></tr>8.Utilities内置在Context中,可以直接通过#访问#dates #calendars #numbers #strings arrays lists sets maps … 5.小结 本文讲述了如何在Spring Boot中引入模板引擎Thymeleaf以及Thymeleaf基础语法和实际使用本文GitHub地址:https://github.com/ishuibo/Sp… ...

April 3, 2019 · 1 min · jiezi

微服务落地,我们在考虑什么?

导读:微服务已经成为过去几年软件架构设计的“事实标准”,大多数企业在推动内部数字化转型的过程中,服务软件系统开始由单一或者SOA服务向微服务转型。那么转型过程需要遵循哪些原则呢?本文结合过往博云微服务落地实践经验,分享微服务落地实践的过程中思考。目前当技术人员提及微服务的时候,首先想到的是Spring Cloud、Dubbo等实现服务的技术框架。这在我们采用微服务的初期阶段是最先考虑的因素。可是随着服务化的进行,我们并没有享受到由框架的便利性与快捷性所带来的业务突飞猛进的成就感。恰恰相反,过多的服务化以及服务间冗余且多元化通信机制反而加重了业务处理的负担。这必然不是我们想要的微服务,却是大多数企业在执行的微服务。因此我们开始重新审视整个行业,审视微服务的发展历程。与过往不同的是:前期阶段,我们把更多的精力投入到业务上,而一定程度上“忽略”技术,因为此时我们建立的信念是无论何种形式的“服务形态”一定是为业务服务的。当我们站在全局的角度,观看整理后的服务,发现了一个及其优美的图形化结构,各个节点的边界清晰,职责分明;节点间的链路畅通,协议规整。这时我们知道我们终于走在了正确的道路上。我们遵循的原则当经过一定时间的挣扎以后,我们觉得微服务的关注点不在于技术本身,但并不意味着不关注技术。在反思过程中,我们认为微服务实践中有两个原则不能变:服务一定是围绕业务的,服务的交互是标准的。我们把原则分为两个阶段:初期阶段,实践阶段。1.初期阶段初期阶段,遵循第一条原则,服务一定是围绕业务的。微服务初期阶段,重要的是业务梳理,而不是花费大量时间在RPC、Service Discovery、 Circuit Breaker这些概念或者Eureka,Docker,Gateway,Dubbo等技术框架的调研上,此时我们重心关注服务的边界与职责划分。这是遵循的两条原则:1、保证单一业务服务高效聚合;2、降低服务间的相互调用(此举是避免陷入大量分布式业务的处理)。这样的原则下,DDD为我们提供了帮助,也依据业务本身的特性实现了服务初期阶段的整理。同时我们发现就算借助DDD的指导,在不同的业务应用中,各个服务也有不同的聚合形态和调用方式。因此我们觉得微服务本身没有一成不变的模式,一切都是围绕业务动态变化的。合理性也仅仅体现在一定阶段的时间范围之内。2.实践阶段当业务建模完成,我们能够清晰的知道各个业务的职责以及与其他业务的关联关系,从理论层面我们完成了业务微服务建模。此时我们开始着手服务的落地实践,在落地实践阶段我们更多关注点同样不在于技术框架,而是在于技术框架的内涵-即服务交互标准。此时我们遵循了第二条原则:服务的交互是标准的。所谓服务交互标准从三个层面解读:协议标准,框架标准,接口标准。·协议标准目前网络应用的协议比较复杂,我们希望选取能够符合业务场景的协议作为通信标准。因此我们考虑了统一的认证鉴权协议、加密解密协议、内部接口交互协议,外围接口服务协议等,各个协议各司其职,用来支撑服务通信的标准化。协议标准不仅仅为平台自身服务,同时与其他业务单元进行通信时,只需要遵循协议标准,就可以实现业务单元之间的快速联动。·框架标准为了支撑业务,我们没有依赖任何的自动化代码生成框架,而是根据我们的协议支持情况,选择最小的服务运行框架,来构建统一的业务单元支撑框架。这里需要说明的一点,框架标准需要考虑业务特性,协议特性,不能一概而论,因此它的有效性也许只在当前构建的业务平台本身。构建标准框架的好处是针对应用内的所有业务单元可以快速复制,简化因为各自开发框架不同导致联调阶段出现问题。·接口标准接口分两种:业务内部接口和业务服务接口。无论哪种接口同样遵循标准化原则。业务内部接口的核心在于压缩协议,加快业务的处理流程,因此可以采用RPC等高效率的协议支持的接口模式;业务服务接口的核心在于表明业务携带的信息,因此采用restful接口规范更合适一些。接口设计需要涵盖但不限于标准化的请求方式、统一的参数处理、统一的结果返回、统一的异常处理、统一的日志处理等。服务拆分与聚合前提:服务拆分与聚合在本篇文章中暂时不考虑web的微服务化设计,只说明后端服务的拆分与聚合实践。服务拆分与聚合需要遵循的原则:服务一定是围绕业务的。但事实情况是,在现在追求“开源整合”的背景下,纯粹的业务单元在不借助第三方工具的前提下,需要消耗巨大的代价才能实现业务需求,同时也出现不同业务单元对同一个工具的强依赖性。因此在服务拆分与聚合时,我们考虑了两种形态的实现方式:业务支撑与工具支撑。·业务支撑业务支撑需要考虑的是业务服务对象和业务内部逻辑。业务服务对象作为整个业务单元的对外形态,通过命名能够清晰的表达其涵盖的业务范围;业务内部逻辑需要对业务单元进行细粒度的拆分,类似一个实体类可以包括多个其他相关联的实体对象(当然如果服务拆分的足够细化,也可以把内部逻辑作为独立的业务单元,但是这样会加重业务直接的通信负载)。基于业务内部逻辑构建业务服务对象的真实场景。具体的拆分细节可以依赖DDD的实践方法进行(当然也需要根据业务做相应调整,没有普世之道)。·工具支撑工具支撑需要结合业务考虑,分为两种:通用性工具和专用性工具。通用性工具旨在为所有业务单元运行提供统一的支撑平台,从而减少由于工具维护花费的精力,使得业务开发人员聚焦业务实现,一般通用工具包包括统一日志处理,统一拦截处理,返回数据统一封装,异常统一处理等等;专用性工具聚焦某个具体的业务单元,由业务单元自身维护(例如迭代升级)。工具支撑层面不会提供对外restful或者rpc的接口,对外的表现形式为编译好的依赖工具包(例如Github的管理接口的封装)。服务架构选型依照执行原则完成服务拆分以后,我们需要考虑的是合适的落地选型。选型方案要考虑的因素有很多:技术背景(尤其是团队内编程语言的设定),服务支撑工具(注册中心,网关,服务调用,负载均衡数据库等),服务运行工具(tomcat,jetty,jboss等),服务部署工具(物理部署,虚拟化,容器等),工具的协议支撑集合(http,rpc,mtqq,idoc等)。但是无论如何选型最终一定要结合团队开发人员当下的技能支撑,这也是我们选型的核心因素,因为白盒相对来说始终比黑盒安全,也相对可控。这里给出我们的技术栈选型框架(仅限我们熟悉的内容),暂时不涉及技术框架的对比说明。服务开发框架:springboot,dubbo,grpc,ServiceMesh(基于ServiceMesh的开发服务框架)分布式存储/注册中心:Zookeeper,Consul,Eureka,Etcd服务网关:Kong,Openresty,Spring cloud Zuul,Spring cloud gateway负载均衡:nginx,spring cloud Ribbon,haproxy,Kubernetes service服务远程调用:Spring cloud feign缓存服务:memchace,redis数据库:mariadb,mysql消息服务:RabbitMQ,NATS,Kafka配置中心:spring cloud config,Apollo,Consul事件机制:Cloud Event服务编排:Conductor ,Kubernetes服务治理:spring cloud,Dubbo,ServiceMesh基于消息机制的分布式事务处理(遵循CAP或者BASE理论模型的实现)业务运行工具:jvm,nginx或者其他可运行环境支撑开发编译工具:Jenkins,maven,gitlab接口文档:Swagger部署工具:物理部署(jar包或者可运行的编译的二进制文件)虚拟化部署(虚拟镜像模板)容器化部署(Docker)我们在落地的过程中,根据团队技术特点开发阶段重点选择了Spring Cloud中涵盖的技术栈。方便易用,能够快速入手。运行阶段选择具备服务编排能力的Kubernetes容器化运行环境,并且结合Devops工具链能够快速迭代部署。服务接口设计服务接口是对外展现业务逻辑的唯一入口,接口定义的规范与否也是微服务落地的关键指标之一,我们在实践的过程中参考了多个开源项目的接口设计,针对任何一个资源对象,整体分为几类场景:资源集合类操作,资源实体操作,异常处理,参数处理,统一数据返回,审计日志以及其他具体场景。统一的接口请求与响应标准其中业务单元绝大多数端口围绕着资源集合类、资源实体类进行操作,因此我们从restful接口规范出发,结合具体场景,规范了请求方式,请求url,请求参数,请求header,响应header,响应值等信息。请求参数涵盖默认语义,包括:Get(获取信息),Post(创建),Put(全量修改),Patch(部分修改),Delete(删除)以Students实体对象的新建为例,给出请求与响应标准。URLURL请求包括三部分:请求方式,统一前缀以及具体url,统一前缀具备一定含义的命名规则,包括api申明,供应商标识,版本说明等必要信息,例如: Post /api/cloud/v1/students?exist={skip,replace}请求headertypeaplication/json:用于single和bulk时,用来表示请求数据为json格式application/vnd.ms-excel:从excel格式的文件导入创建Acceptaplication/json:接受json格式的响应数据AuthorizationOauth2.0的access token(bearer token)Accept-Language(可选)可接受的语言,国际化,en-US表示美国英语请求数据格式+类型json格式:{items:[]}请求创建students对象json(表达):请求(批量)创建student对象列表json(表达)请求(批量)创建student信息excel文响应headerContent-Type aplication/jsonContent-Language(可选) 内容语言Last-Modified 数据最近一次修改的时间戳信息响应值Success message:多种类型Error message:多种类型Exception:多种类型统一异常处理统一异常处理包括状态码以及状态码涵盖的异常信息,具体部分定义如下:200/201+success message(含资源数量信息+uri信息):创建成功,适用于数量不多(比如小于500)的创建操作,大于设定的值时进行异步处理,参加返回值202202+success message with status uri:异步处理,返回进度查询资源uri(/api/vendor/v1/status/{id})400+success+errors(含出错项index的错误列表):批量创建时部分成功,返回成功信息和错误信息401+exception{error_code+message}:缺乏认证信息403+exception{error_code+message}:未授权访问,访问被拒绝406+exception{ error_code+message}:不支持client要求的格式或语言时返回该信息(Not Acceptable)415+exception{error_code+message}:请求中的文档格式不支持422+exception{error_code+message}:不能处理的数据,比如json格式错误、文件内容项错误或会破坏业务规则429+exception{ error_code+message}:太多请求,流控时使用 500+exception{error_code+message}:服务器内部错误统一日志拦截基于AOP模式拦截所有请求,在请求入站与出站的时候,做统一日志记录以及需要的其他非业务处理(例如鉴权)统一的数据返回标准我们参考Restful数据返回标准,封装我们自己的数据返回格式:code,message,body,error,统一的数据返回格式可以在接口层做统一的拦截处理。实现返回数据的标准化。 code:返回状态码 message:返回响应结果的语义解释 body:响应的具体数据信息,包括metada信息,具体响应数据以及请求连接 error:代表返回的错误信息 具体的响应格式如下所示:{“code”: 200,“message”: “获取学生列表成功”,“body”: { “links”: [ { “rel”: “self”, “href”: “http://localhost:8080/api/cloud/v1/students?name=test&startDate=2019-01-01&endDate=2019-09-01&style=normal&sort=asc&limit=10&offset=0{&fields}”, “hreflang”: null, “media”: null, “title”: null, “type”: null, “deprecation”: null } ], “metadata”: [] “content”: [ { “id”: 1, “name”: “test3”, “status”: “running”, “props”: “test”, “remark”: “test”, “ownerId”: 1, “createrId”: 1, “menderId”: 1, “gmtCreate”: “2019-03-11 10:42:15”, “gmtModify”: null, “startDate”: null, “endDate”: null, “links”: [ { “rel”: “self”, “href”: “http://localhost:8080/api/cloud/v1/students/1?style=normal&fields=”, “hreflang”: null, “media”: null, “title”: null, “type”: null, “deprecation”: null } ] } ]}“errors”: {}}服务接口的设计一定是围绕标准化的规则进行的,这样才能在后期减少因为接口变动导致不断出现的前后端联调问题。因为在实践中我们经常遇到格式不统一导致web要写不同的数据解析方式,从而造成大量重复的工作。遗留问题当然我们落地过程的选择也不一定尽善尽美,也有很多随着业务处理能力的加强而在之前没有考虑到的问题,例如:·各个服务自身并发数据支撑能力·服务交互的内部代码瓶颈,包括调用链路冗余,响应偏慢等·数据库的并发支撑与性能优化·与容器服务集成的参数配置,开发与部署环境的转变·调用链路可能出现的回环问题,交叉的业务单元调用,导致调用链路混乱·数据的缓存设计,加快业务响应速率这些问题我们在后续不断深入地理解和探索中会找到相应的解决方案,大家可以在后续继续关注我们的微服务解决方案。 ...

April 3, 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 官方文档完整翻译

以下所有文档均包含多个版本,并支持多语言(英文及中文)。Spring Boot 中文文档Spring Framework 中文文档Spring Cloud 中文文档Spring Security 中文文档Spring Session 中文文档Spring AMQP 中文文档Spring DataSpring Data JPASpring Data JDBCSpring Data RedisContributing如果你希望参与文档的校对及翻译工作,请在 这里 提 PR。

April 3, 2019 · 1 min · jiezi

springboot 控制器接收json对象

添加依赖 <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> <classifier>jdk15</classifier> </dependency>controller 添加代码 public ResultVo deleteSomeUser(@RequestBody JSONObject jsonObject){ List<String> ids = (List)jsonObject.getJSONArray(“ids”); userService.deleteSomeUser(ids); return new ResultVo(“200”,“删除成功”); }

April 2, 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

prometheus 集成dubbo

dubbo 自身的监控使用了dubbo 的拦截器,这里我们也使用dubbo 的拦截器来添加prometheus 监控首先需要dubbo 项目提供http的接口,为dubbo 项目添加 web依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>加入 micrometer prometheus <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>实现dubbo 的filter 接口,添加 @Activate(group = Constants.PROVIDER)注解,声明拦截所有服务提供者@Activate(group = Constants.PROVIDER)public class PrometheusFilter implements Filter { private Logger logger = LoggerFactory.getLogger(PrometheusFilter.class); @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { logger.info("—————-prometheus filter—————"); RequestTimeCollector requestTimeCollector = (RequestTimeCollector) ServiceBean.getSpringContext(). getBean(“dubboRequestTimeCollector”); RpcContext context = RpcContext.getContext(); boolean isProvider = context.isProviderSide(); String serviceName = invoker.getInterface().getName(); String methodName = RpcUtils.getMethodName(invocation); long start = System.currentTimeMillis(); try { // proceed invocation chain Result result = invoker.invoke(invocation); long duration = System.currentTimeMillis() - start; String status = “success”; if(result.getException()!=null){ status = result.getException().getClass().getSimpleName(); } requestTimeCollector.setValue(duration,serviceName,methodName,status); return result; } catch (RpcException e) { long duration = System.currentTimeMillis() - start; String result = “error”; if (e.isTimeout()) { result = “timeoutError”; } if (e.isBiz()) { result = “bisError”; } if (e.isNetwork()) { result = “networkError”; } if (e.isSerialization()) { result = “serializationError”; } requestTimeCollector.setValue(duration,serviceName,methodName,result); throw e; } }}配置拦截器扩展在 resourceMETA-INFdubbo 文件夹下创建com.alibaba.dubbo.rpc.Filter 文本文件添加 prometheus=com.rcplatform.livechat.dubbo.filter.PrometheusFilter文本启动项目访问/actuator/prometheus,即可看到监控项 ...

March 31, 2019 · 1 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 Security项目Spring MVC开发RESTful API(二)

查询请求常用注解@RestController 标明此Controller提供RestAPI@RequestMapping 映射http请求url到java方法@RequestParam 映射请求参数到java方法到参数@PageableDefault 指定分页参数默认值编写一个简单的UserController类@RestController@RequestMapping(value = “/user”)public class UserController { @RequestMapping(method = RequestMethod.GET) public List<User> query(@RequestParam(name = “username”,required = true) String username, @PageableDefault(page = 1,size = 20,sort = “username”,direction = Sort.Direction.DESC)Pageable pageable){ System.out.println(pageable.getSort()); List<User>users=new ArrayList<>(); users.add(new User(“aaa”,“111”)); users.add(new User(“bbb”,“222”)); users.add(new User(“ddd”,“333”)); return users; }}@PageableDefault SpingData分页参数 page当前页数默认0开始 sizi每页个数默认10 sort 排序Srping boot 测试用例在demo的pom.xml里面引入spirngboot的测试 <!–spring测试框架–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>测试/user接口@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //测试用例 @Test public void whenQuerSuccess() throws Exception { String result=mockMvc.perform(MockMvcRequestBuilders.get("/user") //传过去的参数 .param(“username”,“admin”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回的集合的长度是否是3 .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3)) //打印信息 .andDo(MockMvcResultHandlers.print()) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }jsonPath文档语法查询地址用户详情请求常用注解@PathVariable 映射url片段到java方法参数@JsonView 控制json输出内容实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String username; private String password; @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }}Controller类@RestController@RequestMapping(value = “/user”)public class UserController { @RequestMapping(value = “/{id:\d+}",method = RequestMethod.GET) // 正则表达式 :\d+ 表示只能输入数字 //用户名密码都显示 @JsonView(User.UserDetailView.class) public User userInfo(@PathVariable String id){ User user=new User(); user.setUsername(“tom”); return user; }}测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户详情用例 @Test public void whenUserInfoSuccess() throws Exception { String result=mockMvc.perform(MockMvcRequestBuilders.get("/user/1”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.username").value(“tom”)) //打印信息 .andDo(MockMvcResultHandlers.print()) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }}用户处理创建请求常用注解@RequestBody 映射请求体到java方法到参数@Valid注解和BindingResult验证请求参数合法性并处理校验结果实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String id; private String username; //不允许password为null @NotBlank private String password; private Date birthday; @JsonView(UserSimpleView.class) public String getId() { return id; } public void setId(String id) { this.id = id; } @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @JsonView(UserSimpleView.class) public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; }}Controller类 @RequestMapping(method = RequestMethod.POST) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User createUser(@Valid @RequestBody User user, BindingResult errors){ //如果校验有错误是true并打印错误信息 if(errors.hasErrors()){ errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage())); } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户创建用例 @Test public void whenCreateSuccess() throws Exception { Date date=new Date(); String content="{"username":"tom","password":null,"birthday":"+date.getTime()+"}"; String result=mockMvc.perform(MockMvcRequestBuilders.post("/user") .content(content) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(“1”)) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }}修改和删除请求验证注解| 注解 | 解释 | | ——– | ——– | | @NotNull | 值不能为空 | | @Null | 值必须为空 | | @Pattern(regex=) | 字符串必须匹配正则表达式 | | @Size(min=,max=) | 集合的元素数量必须在min和max之间 | | @Email | 字符串必须是Email地址 | | @Length(min=,max=) | 检查字符串长度 | | @NotBlank | 字符串必须有字符 | | @NotEmpty | 字符串不为null,集合有元素 | | @Range(min=,max=) | 数字必须大于等于min,小于等于max | | @SafeHtml | 字符串是安全的html | | @URL | 字符串是合法的URL | | @AssertFalse | 值必须是false | | @AssertTrue | 值必须是true | | @DecimalMax(value=,inclusive) | 值必须小于等于(inclusive=true)/小于(inclusive=false) value指定的值 | | @DecimalMin(value=,inclusive) | 值必须大于等于(inclusive=true)/大于(inclusive=false) value指定的值 | | @Digits(integer=,fraction=) | integer指定整数部分最大长度,fraction小数部分最大长度 | | @Future | 被注释的元素必须是一个将来的日期 | | @Past | 被注释的元素必须是一个过去的日期 | | @Max(value=) | 值必须小于等于value值 | | @Min(value=) | 值必须大于等于value值 |自定义注解修改请求实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String id; //自定义注解 @MyConstraint(message = “账号必须是tom”) private String username; //不允许password为null @NotBlank(message = “密码不能为空”) private String password; //加验证生日必须是过去的时间 @Past(message = “生日必须是过去的时间”) private Date birthday; @JsonView(UserSimpleView.class) public String getId() { return id; } public void setId(String id) { this.id = id; } @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @JsonView(UserSimpleView.class) public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; }}Controller类 @RequestMapping(value = “/{id:\d+}",method = RequestMethod.PUT) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User updateUser(@Valid @RequestBody User user, BindingResult errors){ //如果校验有错误是true并打印错误信息 if(errors.hasErrors()){ errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage())); } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户修改用例 @Test public void whenUpdateSuccess() throws Exception { //当前时间加一年 Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); String content = “{"id":"1","username":"44","password":null,"birthday":” + date.getTime() + “}”; String result = mockMvc.perform(MockMvcRequestBuilders.put("/user/1”) .content(content) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(“1”)) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }自定义注解MyConstraint类import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;//作用在字段跟方法上面@Target({ElementType.FIELD,ElementType.METHOD})//运行时注解@Retention(RetentionPolicy.RUNTIME)//需要校验注解的类@Constraint(validatedBy = MyConstraintValidator.class)public @interface MyConstraint { String message() default “{org.hibernate.validator.constraints.NotBlank.message}”; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};}MyConstraintValidator类import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;//范型1.验证的注解 2.验证的数据类型public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object> { @Override public void initialize(MyConstraint myConstraint) { //校验器初始化的规则 } @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { //校验username如果是tom验证通过 if (value.equals(“tom”)){ return true; }else{ return false; } }}删除请求Controller类 @RequestMapping(value = “/{id:\d+}",method = RequestMethod.DELETE) //@Valid启用校验password不允许为空 public void deleteUser(@PathVariable String id){ System.out.println(id); }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户删除用例 @Test public void whenDeleteSuccess() throws Exception { mockMvc.perform(MockMvcRequestBuilders.delete("/user/1”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()); }服务异常处理把BindingResult errors去掉 @RequestMapping(method = RequestMethod.POST) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User createUser(@Valid @RequestBody User user){ //如果校验有错误是true并打印错误信息// if(errors.hasErrors()){// errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));// } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }查看返回的异常信息处理状态码错误创建文件结构如下404错误将跳转对应页面RESTful API的拦截过滤器(Filter)创建filter文件@Componentpublic class TimeFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println(“TimeFilter init”); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println(“TimeFilter doFilter”); long start=new Date().getTime(); filterChain.doFilter(servletRequest,servletResponse); System.out.println(“耗时”+(new Date().getTime()-start)); } @Override public void destroy() { System.out.println(“TimeFilter destroy”); }}自定义filter需要吧filter文件@Component标签去除@Configurationpublic class WebConfig { @Bean public FilterRegistrationBean timeFilterRegistration(){ FilterRegistrationBean registration=new FilterRegistrationBean(); TimeFilter timeFilter=new TimeFilter(); registration.setFilter(timeFilter); //filter作用的地址 List<String>urls=new ArrayList<>(); urls.add("/user"); registration.setUrlPatterns(urls); return registration; }}拦截器(Interceptor)创建Interceptor文件@Componentpublic class TimeInterceptor implements HandlerInterceptor { //控制器方法调用之前 @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println(“preHandle”); System.out.println(“进入方法”+((HandlerMethod)o).getMethod().getName()); httpServletRequest.setAttribute(“startTime”,new Date().getTime()); //是否调用后面的方法调用是true return true; } //控制器方法被调用 @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { System.out.println(“postHandle”); Long start= (Long) httpServletRequest.getAttribute(“startTime”); System.out.println(“time interceptor耗时”+(new Date().getTime()-start)); } //控制器方法完成之后 @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { System.out.println(“afterCompletion”); System.out.println(“exception is”+e); }}把过滤器添加到webconfig文件@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter { @Autowired private TimeInterceptor timeInterceptor; //过滤器 @Bean public FilterRegistrationBean timeFilterRegistration(){ FilterRegistrationBean registration=new FilterRegistrationBean(); TimeFilter timeFilter=new TimeFilter(); registration.setFilter(timeFilter); //filter作用的地址 List<String>urls=new ArrayList<>(); urls.add("/user/"); registration.setUrlPatterns(urls); return registration; } //拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(timeInterceptor); }}切片(Aspect)@Aspect@Componentpublic class TimeAspect { //@Befor方法调用之前 //@After()方法调用 //@AfterThrowing方法调用之后 //包围,覆盖前面三种 @Around(“execution( com.guosh.web.controller.UserController.(..))”)//表达式表示usercontroller里所有方法其他表达式可以查询切片表达式 public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable { System.out.println(“time aspect start”); //可以获取到传入参数 Object[]args=pjp.getArgs(); for (Object arg: args) { System.out.println(“arg is”+arg); } long start=new Date().getTime(); //相当于filter里doFilter方法 Object object=pjp.proceed(); System.out.println(“time aspect耗时”+(new Date().getTime()-start)); System.out.println(“time aspect end”); return object; }}总结过滤器Filter :可以拿到原始的http请求与响应信息拦截器Interceptor :可以拿到原始的http请求与响应信息还可以拿到处理请求方法的信息切片Aspect :可以拿到方法调用传过来的值使用rest方式处理文件服务返回的上传文件后路径对象在application.yml里添加上传地址#上传文件路径uploadfiledir: filePath: /Users/shaohua/webapp/guoshsecurity@Data@NoArgsConstructor@AllArgsConstructorpublic class FileInfo { private String path;}@RestController@RequestMapping("/file")public class FileController { @Value("${uploadfiledir.filePath}") private String fileDataStorePath;//文件上传地址 @RequestMapping(method = RequestMethod.POST) public FileInfo upload(@RequestParam(“file”) MultipartFile file) throws IOException { //文件名 System.out.println(file.getOriginalFilename()); //文件大小 System.out.println(file.getSize()); //获取文件后缀名 String ext=StringUtils.getFilenameExtension(file.getOriginalFilename()); File fileDir = new File(fileDataStorePath); //判断是否创建目录 if (!fileDir.exists()) { if (!fileDir.mkdirs() || !fileDir.exists()) { // 创建目录失败 throw new RuntimeException(“无法创建目录!”); } } File localFile=new File(fileDataStorePath, UUID.randomUUID().toString().replace("-", “”)+"."+ext); file.transferTo(localFile); //返回上传的路径地址 return new FileInfo(localFile.getAbsolutePath()); } //下载文件 @RequestMapping(value ="/{id}" ,method = RequestMethod.GET) public void download(@PathVariable String id, HttpServletResponse response){ //模拟下载直接填好了下载文件名称 try(InputStream inputStream = new FileInputStream(new File(fileDataStorePath,“13a2c075b7f44025bbb3c590f7f372eb.txt”)); OutputStream outputStream=response.getOutputStream();){ response.setContentType(“application/x-download”); response.addHeader(“Content-Disposition”,“attachment;filename="+“13a2c075b7f44025bbb3c590f7f372eb.txt"”); IOUtils.copy(inputStream,outputStream); } catch (Exception e) { e.printStackTrace(); } }}使用Swagger工具在demo模块引入 <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>添加swagger的配置类@Configuration@EnableSwagger2public class Swagger2Config { @Value("${sys.swagger.enable-swgger}”) private Boolean enableSwgger; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .enable(enableSwgger) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage(“com.guosh.web”)) //swgger插件作用范围 //.paths(PathSelectors.regex("/api/.")) .paths(PathSelectors.any()) //过滤接口 .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(“SpringSecurityDemo”) //标题 .description(“API描述”) //描述 .contact(new Contact(“guoshaohua”, “http://www.guoshaohua.cn”, “”))//作者 .version(“1.0”) .build(); }}常用注解通过@Api用于controller类上对类的功能进行描述通过@ApiOperation注解用在controller方法上对类的方法进行描述通过@ApiImplicitParams、@ApiImplicitParam注解来给参数增加说明通过@ApiIgnore来忽略那些不想让生成RESTful API文档的接口通过@ApiModel 用在返回对象类上描述返回对象的意义通过@ApiModelProperty 用在实体对象的字段上 用于描述字段含义 ...

March 30, 2019 · 6 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

Springboot定时任务踩坑记录

前言在使用Springboot整合定时任务,发现当某个定时任务执行出现执行时间过长的情况时会阻塞其他定时任务的执行。问题定位后续通过翻查Springboot的文档以及打印日志(输出当前线程信息)得知问题是由于Springboot默认使用只要1个线程处理定时任务。问题复盘需要注意示例的Springboot版本为2.1.3.RELEASE。关键pom文件配置 <!–继承父项目–> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> …省略非关键配置 <!– 引入依赖–> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>定时任务import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;/** * 定时任务 * @author RJH * create at 2019-03-29 /@Componentpublic class SimpleTask { private static Logger logger= LoggerFactory.getLogger(SimpleTask.class); /* * 执行会超时的任务,定时任务间隔为5000ms(等价于5s) / @Scheduled(fixedRate = 5000) public void overtimeTask(){ try { logger.info(“current run by overtimeTask”); //休眠时间为执行间隔的2倍 Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } /* * 正常的定时任务 / @Scheduled(fixedRate = 5000) public void simpleTask(){ logger.info(“current run by simpleTask”); }}启动类import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication@EnableSchedulingpublic class TaskDemoApplication { public static void main(String[] args) { SpringApplication.run(TaskDemoApplication.class, args); }}运行结果…省略非关键信息2019-03-29 21:22:38.410 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by simpleTask2019-03-29 21:22:38.413 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by overtimeTask2019-03-29 21:22:48.413 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by simpleTask2019-03-29 21:22:48.414 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by overtimeTask2019-03-29 21:22:58.418 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by simpleTask2019-03-29 21:22:58.418 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by overtimeTask2019-03-29 21:23:08.424 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by simpleTask2019-03-29 21:23:08.424 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by overtimeTask2019-03-29 21:23:18.425 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by simpleTask2019-03-29 21:23:18.426 INFO 59731 — [ scheduling-1] com.rjh.task.SimpleTask : current run by overtimeTask…结果分析由运行结果可以看出:每次定时任务的运行都是由scheduling-1这个线程处理正常运行的simpleTask被overtimeTask阻塞导致了运行间隔变成了10秒后面通过查阅Springboot的文档也得知了定时任务默认最大运行线程数为1。解决方案由于使用的Springboot版本为2.1.3.RELEASE,所以有两种方法解决这个问题使用Springboot配置在配置文件中可以配置定时任务可用的线程数:## 配置可用线程数为10spring.task.scheduling.pool.size=10自定义定时任务的线程池使用自定义的线程池代替默认的线程池import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.TaskScheduler;import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;/* * 定时任务配置类 * @author RJH * create at 2019-03-29 /@Configurationpublic class ScheduleConfig { /* * 此处方法名为Bean的名字,方法名无需固定 * 因为是按TaskScheduler接口自动注入 * @return */ @Bean public TaskScheduler taskScheduler(){ // Spring提供的定时任务线程池类 ThreadPoolTaskScheduler taskScheduler=new ThreadPoolTaskScheduler(); //设定最大可用的线程数目 taskScheduler.setPoolSize(10); return taskScheduler; }} ...

March 29, 2019 · 2 min · jiezi

encodeURIComponent(参数编码格式)

如果有+一般通过路由参数传到后台会转义成空格(一般id出现较多)

March 29, 2019 · 1 min · jiezi

面试分享:最全Spring事务面试考点整理

Spring和事务的关系关系型数据库、某些消息队列等产品或中间件称为事务性资源,因为它们本身支持事务,也能够处理事务。 Spring很显然不是事务性资源,但是它可以管理事务性资源,所以Spring和事务之间是管理关系。 就像Jack Ma虽然不会写代码,但是他却管理者一大批会写代码的码农。Spring事务三要素数据源:表示具体的事务性资源,是事务的真正处理者,如MySQL等。事务管理器:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。事务应用和属性配置:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属性如隔离级别、超时时间等。Spring事务的注解配置把一个DataSource(如DruidDataSource)作为一个@Bean注册到Spring容器中,配置好事务性资源。把一个@EnableTransactionManagement注解放到一个@Configuration类上,配置好事务管理器,并启用事务管理。把一个@Transactional注解放到类上或方法上,可以设置注解的属性,表明该方法按配置好的属性参与到事务中。事务注解的本质@Transactional这个注解仅仅是一些(和事务相关的)元数据,在运行时被事务基础设施读取消费,并使用这些元数据来配置bean的事务行为。大致来说具有两方面功能,一是表明该方法要参与事务,二是配置相关属性来定制事务的参与方式和运行行为。Spring声明式事务实现原理声明式事务成为可能,主要得益于Spring AOP。使用一个事务拦截器,在方法调用的前后/周围进行事务性增强(advice),来驱动事务完成。如何回滚一个事务就是在一个事务上下文中当前正在执行的代码里抛出一个异常,事务基础设施代码会捕获任何未处理的异常,并且做出决定是否标记这个事务为回滚。默认回滚规则默认只把runtime, unchecked exceptions标记为回滚,即RuntimeException及其子类,Error默认也导致回滚。Checked exceptions默认不导致回滚。这些规则和EJB是一样的。如何配置回滚异常使用@Transactional注解的rollbackFor/rollbackForClassName属性,可以精确配置导致回滚的异常类型,包括checked exceptions。noRollbackFor/noRollbackForClassName属性,可以配置不导致回滚的异常类型,当遇到这样的未处理异常时,照样提交相关事务。事务注解在类/方法上@Transactional注解既可以标注在类上,也可以标注在方法上。当在类上时,默认应用到类里的所有方法。如果此时方法上也标注了,则方法上的优先级高。事务注解在类上的继承性@Transactional注解的作用可以传播到子类,即如果父类标了子类就不用标了。但倒过来就不行了。子类标了,并不会传到父类,所以父类方法不会有事务。父类方法需要在子类中重新声明而参与到子类上的注解,这样才会有事务。事务注解在接口/类上@Transactional注解可以用在接口上,也可以在类上。在接口上时,必须使用基于接口的代理才行,即JDK动态代理。事实是Java的注解不能从接口继承,如果你使用基于类的代理,即CGLIB,或基于织入方面,即AspectJ,事务设置不会被代理和织入基础设施认出来,目标对象不会被包装到一个事务代理中。Spring团队建议注解标注在类上而非接口上。只在public方法上生效?当采用代理来实现事务时,(注意是代理),@Transactional注解只能应用在public方法上。当标记在protected、private、package-visible方法上时,不会产生错误,但也不会表现出为它指定的事务配置。可以认为它作为一个普通的方法参与到一个public方法的事务中。如果想在非public方法上生效,考虑使用AspectJ(织入方式)。目标类里的自我调用没有事务?在代理模式中(这是默认的),只有从外部的方法调用进入通过代理会被拦截,这意味着自我调用(实际就是,目标对象中的一个方法调用目标对象的另一个方法)在运行时不会导致一个实际的事务,即使被调用的方法标有注解。如果你希望自我调用也使用事务来包装,考虑使用AspectJ的方式。在这种情况下,首先是没有代理。相反,目标类被织入(即它的字节码被修改)来把@Transactional加入到运行时行为,在任何种类的方法上都可以。事务与线程和JavaEE事务上下文一样,Spring事务和一个线程的执行相关联,底层是一个ThreadLocal<Map<Object, Object>>,就是每个线程一个map,key是DataSource,value是Connection。逻辑事务与物理事务事务性资源实际打开的事务就是物理事务,如数据库的Connection打开的事务。Spring会为每个@Transactional方法创建一个事务范围,可以理解为是逻辑事务。在逻辑事务中,大范围的事务称为外围事务,小范围的事务称为内部事务,外围事务可以包含内部事务,但在逻辑上是互相独立的。每一个这样的逻辑事务范围,都能够单独地决定rollback-only状态。那么如何处理逻辑事务和物理事务之间的关联关系呢,这就是传播特性解决的问题。事务的传播特性REQUIRED,SUPPORTS,MANDATORY,REQUIRES_NEW,NOT_SUPPORTED,NEVER,NESTEDREQUIRED强制要求要有一个物理事务。如果没有已经存在的事务,就专门打开一个事务用于当前范围。或者参与到一个已存在的更大范围的外围事务中。在相同的线程中,这是一种很好的默认方式安排。(例如,一个service外观/门面代理到若干个仓储方法,所有底层资源必须参与到service级别的事务里)在标准的REQUIRED行为情况下,所有这样的逻辑事务范围映射到同一个物理事务。因此,在内部事务范围设置了rollback-only标记,确实会影响外围事务进行实际提交的机会。注:默认,一个参与到外围事务的事务,会使用外围事务的特性,安静地忽略掉自己的隔离级别,超时值,只读标识等设置。当然可以在事务管理器上设置validateExistingTransactions标识为true,这样当你自己的事务和参与到的外围事务设置不一样时会被拒绝。REQUIRES_NEW与REQUIRED相比,总是使用一个独立的物理事务用于每一个受影响的逻辑事务范围,从来不参与到一个已存在的外围事务范围。这样安排的话,底层的事务资源是不同的,因此,可以独立地提交或回滚。外围事务不会被内部事务的回滚状态影响。这样一个独立的内部事务可以声明自己的隔离级别,超时时间和只读设置,并不继承外围事务的特性。NESTED使用同一个物理事务,带有多个保存点,可以回滚到这些保存点,可以认为是部分回滚,这样一个内部事务范围触发了一个回滚,外围事务能够继续这个物理事务,尽管有一些操作已经被回滚。典型地,它对应于JDBC的保存点,所以只对JDBC事务资源起作用。SUPPORTS支持当前事务。如果当前有事务,就参与进来,如果没有,就以非事务的方式运行。这样的一个逻辑事务范围,它背后可能没有实际的物理事务,此时的事务也成为空事务。NOT_SUPPORTED不支持当前事务。总是以非事务方式运行。当前的事务会被挂起,并在适合的时候恢复。MANDATORY支持当前事务。如果当前没有事务存在,就抛出异常。NEVER不支持当前事务。如果当前有事务存在,就抛出异常。事务的隔离级别DEFAULT,READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ,SERIALIZABLE脏读一个事务修改了一行数据但没有提交,第二个事务可以读取到这行被修改的数据,如果第一个事务回滚,第二个事务获取到的数据将是无效的。不可重复读一个事务读取了一行数据,第二个事务修改了这行数据,第一个事务重新读取这行数据,将获得到不同的值。幻读一个事务按照一个where条件读取所有符合的数据行,第二个事务插入了一行数据且恰好也满足这个where条件,第一个事务再以这个where条件重新读取,将会获取额外多出来的这一行。帮助记忆:写读是脏读,读写读是不可重复读,where insert where是幻读。DEFAULT使用底层数据存储的默认隔离级别。MySQL的默认隔离级别是REPEATABLE-READ。READ_UNCOMMITTED读未提交。脏读、不可重复读、幻读都会发生。READ_COMMITTED读已提交。脏读不会发生,不可重复读、幻读都会发生。REPEATABLE_READ可重复读。脏读、不可重复读都不会发生,幻读会发生。SERIALIZABLE可串行化。脏读、不可重复读、幻读都不会发生。spring事务考点我就总结在这里了,如果有遗漏或者改进还请各位大佬留言指点同时spring事务这个知识点也为大家总结我的部分学习笔记和与之相匹配的架构进阶视频资料:spring事务部分笔记资料获取方式:请加JAVA架构技术交流群:171662117注:加群要求1、具有1-5工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加。2、在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。3、如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的,可以加。4、觉得自己很牛B,一般需求都能搞定。但是所学的知识点没有系统化,很难在技术领域继续突破的可以加。5.阿里Java高级大牛直播讲解知识点,分享知识,多年工作经验的梳理和总结,带着大家全面、科学地建立自己的技术体系和技术认知!

March 29, 2019 · 1 min · jiezi

基于注解的Spring定时任务配置

Spring版本5.1.5、JDK版本1.8首先有一个定时的任务类package com.yuanweiquan.learn.quartzs;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;@Componentpublic class MyQuartzs { @Scheduled(cron = “/5 * * * * ?”)//每五秒执行一次 public void quartzs() { System.out.println(LocalDateTime.now().toString()); }}XML配置<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns=“http://www.springframework.org/schema/beans" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns:p=“http://www.springframework.org/schema/p" xmlns:context=“http://www.springframework.org/schema/context" xmlns:task=“http://www.springframework.org/schema/task" xmlns:aop=“http://www.springframework.org/schema/aop" xmlns:tx=“http://www.springframework.org/schema/tx" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> //扫描包路劲 <context:component-scan base-package=“com.yuanweiquan.learn."></context:component-scan> //启用定时任务 <task:annotation-driven></task:annotation-driven></beans>启动Spring容器,控制台打印结果如下:2019-03-29T15:15:30.0112019-03-29T15:15:35.0022019-03-29T15:15:40.0012019-03-29T15:15:45.0012019-03-29T15:15:50.0015秒钟打印一次,刚好符合我们的需求。但是如果我们的任务执行时间大于任务间隔时间5s,会怎么样呢?我们打印后设置一个休眠时间public class MyQuartzs { @Scheduled(cron = “*/5 * * * * ?”) public void quartzs() { System.out.println(LocalDateTime.now().toString()); try { Thread.sleep(6000); } catch (Exception e) { e.printStackTrace(); } }}再次启动spring容器,控制台打印结果如下:2019-03-29T15:18:45.0112019-03-29T15:18:55.0012019-03-29T15:19:05.001从打印结果可以看出来,任务10s执行了一次,而不是我们希望的5s。原因是当定时任务准备执行时,发现上次任务还未执行完,就会再次等待休眠时间,再次执行,直到任务可以执行为止。

March 29, 2019 · 1 min · jiezi

中台到底是个啥?

文章来自健荐微信公众号,作者王健,ThoughtWorks高级咨询师。王健将于5月18-19日在上海A2M峰会分享关于中台的话题,更多A2M峰会内容可点击此处查看。从去开始,好像就有一只无形的手一直将我与“微服务”、“平台化”、“中台化”撮合在一起,给我带来了很多的困扰和思考与收获。平台化正兴起,从基础设施到人工智能等各个领域不断涌现的各类平台,对于软件开发人员及企业带来了深远的影响。然而,在中国提“数字化平台战略”大家可能会觉得比较抽象,比较远大空;若是提“中台”大家则会更熟悉一些。那…中台到底是什么?会不会又是另一个Buzzword呢?这个从名字上看像是从前台与后台中间硬挤出来的新断层,它与前台和后台的区别和界限到底在哪儿?什么应该放到中台,什么又应该放到前台或是后台?它的出现到底是为了解决什么问题呢?这一个接一个的问题就不断的涌出并萦绕在我的脑子里。直到一年多后的今天,随着参与的几个平台化、企业中台相关的项目已顺利步上正轨,终于可以坐下来回顾这一年的实践与思考,再次试图回答这些问题,并梳理成文,与大家交流探讨。一、中台迷思到处都在喊中台,到处都是中台,中台这个词在我看来已经被滥用了。在一部分人眼里:中台就是技术平台,像微服务开发框架、DevOps平台、PaaS平台,容器云之类的,人们都叫它“技术中台”;在一部份人眼里:中台就是微服务业务平台,像最常见的什么用户中心、订单中心,各种微服务集散地,人们都叫它“业务中台”;在一些人眼里:中台应该是组织的事情,在释放潜能:平台型组织的进化路线图 (豆瓣)中就提出了平台型组织和组织中台的概念,这类组织中台在企业中主要起到投资评估与投后管理的作用,类似于企业内部资源调度中心和内部创新孵化组织,人们叫它“组织中台”。看完本篇你就会理解,上边的这几类“中台”划分还是靠谱的,更多我看到的情况是,大家为了响应企业的“中台战略”,干脆直接将自己系统的“后端”或是“后台”改个名,就叫“中台”。中台到底是什么?它对于企业的意义到底是什么?当我们谈中台时我们到底在谈些什么?想要寻找到答案,仅仅沉寂在各自“中台”之中,如同管中窥豹,身入迷阵,是很难想清楚的。不如换个⾓度,从各类的“中台迷阵”中跳脱出来,尝试以更高的视角,从企业均衡可持续发展的角度来思考中台,反推它存在的价值。为了搞明白中台存在的价值,我们需要回答以下两个问题:企业为什么要平台化?企业为什么要建中台?1、企业为什么要平台化?先给答案,其实很简单:因为在当今互联网时代,⽤户才是商业战场的中心,为了快速响应用户的需求,借助平台化的力量可以事半功倍。不断快速响应、探索、挖掘、引领⽤户的需求,才是企业得以⽣存和持续发展的关键因素。那些真正尊重用户,甚⾄不惜调整⾃己颠覆⾃己来响应⽤户的企业,将在这场以⽤户为中心的商业战争中得以⽣存和发展;反之,那些在过去的成就上故步⾃封,存在侥幸⼼理希望⽤户会像之前一样继续追随⾃己的企业,则会被用户淘汰。很残酷,但这就是这个时代最基本的企业⽣存法则。平台化之所以重要,就是因为它赋予或加强了企业在以用户为中心的现代商业战争中最最最核心的能力:⽤户响应力。这种能力可以帮助企业在商战上先发制⼈,始终抢得先机。可以说,在互联网时代,商业的斗争就是对于用户响应力的比拼。又有点远大空是不是?我们来看⼏个经典的例子:例子1说起中台,最先想到的应该就属是阿⾥的“⼤中台,⼩前台”战略。阿⾥⼈通过多年不懈的努力,在业务的不断催化滋养下,将⾃己的技术和业务能力沉淀出一套综合能力平台,具备了对于前台业务变化及创新的快速响应能力。例子2海尔也早在⼗年前就已经开始推进平台化组织的转型,提出了“平台⾃营体⽀撑⼀线⾃营体”的战略规划和转型⽬标。构建了“⼈单合一”、“⽤户付薪”的创客文化,真正将平台化提⾼到了组织的⾼度。例子3华为在几年前就提出了“⼤平台炮火支撑精兵作战”的企业战略,“让听得到炮声的人能呼唤到炮火”,这句话形象的诠释了大平台⽀撑下小前台的作战策略。这种极度灵活又威力巨⼤的战法,使之可以迅速响应瞬息万变的战场,一旦锁定目标,通过大平台的炮火群,迅速精准对于战场进行强大的火⼒支援。可⻅,在互联⽹热火朝天,第四次工业革命的曙光即将到来的今日,企业能否真正做到“以用户为中心”,并不断提升自己的用户响应力来追随甚至引领用户的脚步,持续规模化创新,终将决定企业能否在这样充满挑战和机遇的市场上笑到最后,在商业上长久保持创新活力与竞争力。而平台化恰好可以助力企业更快更好的做到这些,所以这回答了第一个问题——企业需要平台化。2、企业为什么要建中台?好,到此我们想明白了为什么需要平台化。但是平台化并不是一个新概念,很多企业在这个方向上已经做了多年的努力和积淀。那为什么最近几年“中台”这个相对较新的概念又会异军突起?对于企业来讲,传统的“前台+后台”的平台化架构又为什么不能满足企业的要求呢?这就引出了我们的第二个问题:企业为什么要建中台?定义一下前台与后台因为平台这个词过于宽泛了,为了能让大家理解我在说什么,我先定义一下本文上下文我所说的前台和后台各指什么:前台:由各类前台系统组成的前端平台。每个前台系统就是一个用户触点,即企业的最终用户直接使用或交互的系统,是企业与最终用户的交点。例如用户直接使用的网站、手机App、微信公众号等都属于前台范畴。后台:由后台系统组成的后端平台。每个后台系统一般管理了企业的一类核心资源(数据+计算),例如财务系统、产品系统、客户管理系统、仓库物流管理系统等,这类系统构成了企业的后台。基础设施和计算平台作为企业的核心计算资源,也属于后台的一部分。后台并不为前台而生定义了前台和后台,对于第二个问题(企业为什么要建中台),同样先给出我的答案:因为企业后台往往并不能很好的支撑前台快速创新响应用户的需求,后台更多解决的是企业管理效率问题,而中台要解决的才是前台的创新问题。大多数企业已有的后台,要么前台根本就用不了,要么不好用,要么变更速度跟不上前台的节奏。我们看到的很多企业的后台系统,在创建之初的目标,并不是主要服务于前台系统创新,更多的是为了实现后端资源的电子化管理,解决企业管理的效率问题。这类系统要不就是当年花大价钱外购,需要每年支付大量的服务费,并且版本老旧,定制化困难;要不就是花大价钱自建,年久失修,一身的补丁,同样变更困难,也是企业所谓的“遗留系统”的重灾区。总结下来就两个字“慢”和“贵”,对业务的响应慢,动不动改个小功能就还要花一大笔钱。有人会说了,你不能拿遗留系统说事儿啊,我们可以新建后台系统啊,整个2.0问题不就解决了?但就算是新建的后台系统,因为其管理的是企业的关键核心数据,考虑到企业安全、审计、合规、法律等限制,导致其同样往往⽆法被前台系统直接使用,或是受到各类限制⽆法快速变化,以⽀持前台快速的创新需求。此时的前台和后台就像是两个不同转速的⻮轮,前台由于要快速响应前端用户的需求,讲究的是快速创新迭代,所以要求转速越快越好;⽽后台由于⾯对的是相对稳定的后端资源,⽽且往系统陈旧复杂,甚至还受到法律法规审计等相关合规约束,所以往往是稳定至上,越稳定越好,转速也自然是越慢越好。所以,随着企业务的不断发展,这种“前台+后台”的⻮轮速率“匹配失衡”的问题就逐步显现出来。随着企业业务的发展壮大,因为后台修改的成本和⻛险较⾼,也就驱使我们尽量选择保持后台系统的稳定性。但因为还要响应用户持续不断的需求,自然就会将大量的业务逻辑(业务能力)直接塞到了前台系统中,引入重复的同时还会致使前台系统不断膨胀,变得臃肿,形成了一个个⼤泥球的“烟囱式单体应用”,渐渐拖垮了前台系统的“⽤户响应⼒”。用户满意度降低,企业竞争力也随之不断下降。对于这样的问题,Gatner在2016年提出的一份《Pace-Layered Application Strategy》报告中,给出了一种解决方案,即按照“步速”将企业的应用系统划分为三个层次(正好契合前中后台的三个层次),不同的层次采用完全不同的策略。而Pace-Layered Application Strategy也为“中台”产生的必然性,提供了理论上的支撑。Pace-Layered Application Strategy在这份报告中Gatner提出,企业构建的系统从Pace-Layered的⾓度来看可以划分为三类: SOR(Systems of record ),SOD(Systems of differentiation)和SOI(Systems of innovation)。处于不同Pace-Layered的系统因为⽬的不同、关注点不同、要求不同,变化的“速率”自然也不同,匹配的也需要采⽤不同的技术架构、管理流程、治理架构甚至投资策略。⽽前面章节我们提到的后台系统,例如CRM、ERP、财务系统等,它们⼤多都处于SOR的Pace-Layered。这些系统的建设之初往往是以规范处理企业底层资源和企业的核⼼可追溯单据(例如财务单据,订单单据)为主要⽬的。它们的变更周期往往比较⻓,⽽且由于法律律审计等其他限制,导致对于它们的变更需要严谨的申报审批流程和更高级别的测试部署要求,这就导致了它们往往变化频率低、变化成本高、变化⻛险高、变化周期⻓,⽆法满⾜由⽤户驱动的快速变化的前台系统要求。我们又要尽力保持后台(SOR)系统的稳定可靠,⼜要前台系统(SOI)能够⼩而美,快速迭代。就出现了上文提到的“齿轮匹配失衡”的问题,感觉鱼与熊掌不可兼得。正当陷入僵局的时候,天空中飘来一声IT谚语:软件开发中遇到的所有问题,都可以通过增加⼀层抽象而得以解决!⾄此,⼀声惊雷滚过,“中台”脚踏七彩祥云,承载着SOD(Systems of differentiation) 的前世寄托,横空出世。我们先试着给中台下个定义:中台是真正为前台而生的平台(可以是技术平台,业务能力甚至是组织机构),它存在的唯一目的就是更好的服务前台规模化创新,进而更好的响应服务引领用户,使企业真正做到自身能力与用户需求的持续对接。中台就像是在前台与后台之间添加的⼀组“变速齿轮”,将前台与后台的速率进行匹配,是前台与后台的桥梁。它为前台而生,易于前台使用,将后台资源顺滑流向用户,响应用户。中台很像Pace-Layered中的SOD,提供了比前台(SOI)更强的稳定性,以及⽐后台(SOR)更高的灵活性,在稳定与灵活之间寻找到了⼀种美妙的平衡。中台作为变速齿轮,链接了用户与企业核心资源,并解决了配速问题:有了“中台”这⼀新的Pace-Layered断层,我们就可以将早已臃肿不堪的前台系统中的稳定通用业务能力“沉降”到中台层,为前台减肥,恢复前台的响应力;又可以将后台系统中需要频繁变化或是需要被前台直接使用的业务能力“提取”到中台层,赋予这些业务能力更强的灵活度和更低的变更成本,从而为前台提供更强大的“能力炮火”支援。所以,企业在平台化的过程中,需要建设自己的中台层(同时包括技术中台、业务中台和组织中台)。3、小结思考并回答了文初提出的两个关于中台价值的核心问题,解决了我对于中台产生的一些困惑,不知道对你有没有启发?我最后再来总结一下:以用户为中心的持续规模化创新,是中台建设的核心目标。企业的业务响应能⼒和规模化创新能力,是互联⽹时代企业综合竞争⼒的核⼼体现。平台化包括中台化,只是帮助企业达到这个目标的⼿段,并不是⽬标本身。中台(无论是技术中台、业务中台还是组织中台)的建设,根本上是为了解决企业响应力困境, 弥补创新驱动快速变化的前台,稳定可靠驱动变化周期相对较慢的后台之间的⽭盾,提供⼀个中间层来适配前台与后台的配速问题,沉淀能⼒,打通并顺滑链接前台需求与后台资源,帮助企业不断提升用户响应⼒。所以,中台到底是什么根本不重要,如何想方设法持续提高企业对于⽤户的响应力才是最重要的。而平台化或是中台化,只是恰巧走在了了这条正确的大道上。二、到底中台长啥样?列举了这么多各式各样的中台,最后都扯到了组织层面,是不是有种越听越晕的感觉?好像什么东西加个“中台”的后缀都可以靠到中台上来?那估计很快就会看到例如AI中台、VR中台、搜索中台、算法中台……对了,算法中台已经有了……让我们引用一段阿里玄难在接受采访时提到对于中台的一段我非常认同的描述:本文中我们一直提到的一个词就是“能力”,从玄难的这段采访也可以看出,在阿里“能力”也是中台的核心。甄别是不是中台,还要回到中台要解决的问题上,也就是我上面主要关注的问题。我认为一切以”以用户为中心的持续规模化创新”为目的,将后台各式各样的资源转化为前台易于使用的能力,帮助我们打赢这场以用户为中心的战争的平台,我们都可以称之为中台:业务中台提供重用服务,例如用户中心、订单中心之类的开箱即用可重用能力,为战场提供了强大的后台炮火支援能力,随叫随到,威力强大;数据中台提供了数据分析能力,帮助我们从数据中学习改进、调整方向,为战场提供了强大及时的雷达监测能力,帮助我们掌控战场;移动及算法中台提供了战场一线火力支援能力,帮助我们提供更加个性化的服务,增强用户体验,为战场提供了陆军支援能力,随机应变,所向披靡;技术中台提供了自建系统部分的技术支撑能力,帮助我们解决了基础设施,分布式数据库等底层技术问题,为前台特种兵提供了精良的武器装备;研发中台提供了自建系统部分的管理和技术实践支撑能力,帮助我们快速搭建项目、管理进度、测试、持续集成、持续交付,是前台特种兵的训练基地及快速送达战场的机动运输部队;组织中台为我们的项目提供投资管理、风险管理、资源调度等,是战场的指挥部,战争的大脑,指挥前线,调度后方。所以,评判一个平台是否称得上中台,最终评判标准不是技术,也不是长什么模样,还是得前台说了算,毕竟前台才是战争的关键,是感受得到战场的残酷、看得见用户的那部分人。前台想不想用,爱不爱用,好不好用;帮了前台多大的忙,从中台获得了多大的好处,愿意掏出多少利润来帮助建设中台,这些才是甄别中台建设对错好坏的标准。对于中台来讲,前台就是用户,以用户为中心,在中台同样适用。三、中台就是「企业级能力复用平台」如果让我给出一个定义,目前我认为最贴切的应该是: 中台就是「企业级能力复用平台」。很简单,有点失望是不是?但是为了找到一个靠谱的定义,我几乎花了快两年的时间,期间有各种各样的定义曾浮现出来,但至少到目前为止,只有这个定义我觉得最贴切、最简单、也最准确,它能解释几乎所有我碰到的关于中台的问题,例如:为什么会有那么多中台,像上文提到业务中台,数据中台,搜索中台,移动中台,哪些才是中台,哪些是蹭热点的?中台与前台的划分原则是什么?中台化与平台化的区别是什么?中台化和服务化的区别是什么?中台该怎么建设?等等…这9个字看起来简单,重要的是其背后对「中台」价值的阐释,下面就让我为大家一一拆解来看。企业级当做中台建设的时候,一定是跳出单条业务线站在企业整体视角来审视业务全景,寻找可复用的能力进行沉淀,从而希望通过能力的复用一方面消除数据孤岛业务孤岛,一方面支撑企业级规模化创新,助力企业变革,滋生生态。所以虽然中台的建设过程虽然可以自下而上,以点及面。但驱动力一定是自上而下的,全局视角的,并且需要一定的顶层设计。这也解释了为什么在企业中推动中台建设的往往都是跨业务部门,例如CIO级别领导或是企业的战略规划部门,因为只有这些横跨多条业务线的角色和组织,才会去经常反思与推动企业级的能力复用问题。这一点也引出了中台建设的一个关键难点,就是组织架构的调整和演进以及利益的重新分配,这是技术所不能解决的,也是中台建设的最强阻力。同时企业级也是区分企业中台化与应用系统服务化的关键点,简而言之中台化是企业级、是全局视角,服务化更多是系统级、是局部视角。所以从中台的兴起与爆发可以看到一种趋势,就是越来越多的企业无论是由于企业运营效率的原因还是由于创新发展的需要,对于企业全局视角跨业务线的能力沉淀都提高到了前所未有的战略高度。能力提到中台,最常听到的一个词就是「能力」。可能是因为能力这个词足够简单,又有着足够的包容度与宽度。企业的能力可能包含多个维度,常见的例如计算能力,技术能力,业务能力,数据能力,AI能力,运营能力,研发能力…其中大部分的能力还可以继续细化和二次展开,从而形成一张多维度的企业能力网。可以说,中台就是企业所有可以被「多前台产品团队」复用能力的载体。复用虽然我们一直讲「去重复用」讲了很多年,但仔细想想,大多平台化建设会将重点放在「去重」(消除重复)上,而对于「复用」则没有足够的关注。很多企业都号称已经建成了各种各样成熟的平台,但是我们问心自问一下,有多少平台是业务驱动的?有多少前台产品团队又是自愿将自己的产品接入到平台上的?有多少平台建设者是在真正关注前台产品团队的平台用户体验?「去重」讲的更多是向后看,是技术驱动的;「复用」讲的更多是向前看,是业务驱动和用户驱动的。所以「去重」与「复用」虽然经常一起出现,一起被提及,但是谈论的完全不是一件事情,目的不同,难度也不同,做到「去重」已然非常困难,关注「复用」的就更是寥寥无几,所以:「复用」是中台更加关注的目标;「可复用性」和「易复用性」是衡量中台建设好坏的重要指标;「业务响应力」和「业务满意度」也才是考核中台建设进度的重要标准。而实现更好的复用,常常改进的方向有两个方面:一方面将更高抽象(例如业务模式级别)的通用业务逻辑通过抽象后下沉到中台,这样前台就会更轻,学习成本和开发维护成本更低,越能更快的适应业务变化;缺点是,抽象级别越高,越难被复用,需要架构师对于各业务有深入的理解和非常强的抽象能力。另一方面就是通过对于中台能力的SaaS化包装,减少前台团队发现中台能力和使用中台能力的阻力,甚至通过自助式(Self-Service)的方式就快速定位和使用中台能力。目前很多企业在尝试的内部API集市或是数据商店就是在这方面的努力和尝试。平台这里的平台主要是区别于大单体的应用或是系统。传统的企业数字化规划更多的是围绕业务架构,应用架构和数据架构展开。产出也是一个个基于应用和系统的数字化建设规划,例如要采购或是自建哪些具体的系统,例如ERP、CRM等。当然这个过程并没有什么问题,可以理解此时这些独立的系统就承载了企业的各种能力,由于企业各业务线统一使用一个应用或系统,也自然实现了能力的复用。但问题常常出现在两个方面:一个是大单体系统的业务响应力有限,缺少「柔性」,当业务发展到一定阶段后,必然产生大量定制化需求,随着内部定制化模块的比例逐渐上升,响应力成指数下降,成为业务的瓶颈点。另一个则是系统间的打通通常比较困难,容易形成业务孤岛和数据孤岛。所以越来越多的企业开始像互联网学习,以平台化的方式重塑企业IT架构,从而对于业务提供足够的「柔性」,来满足对于业务的快速响应和复用的需求。小结「企业级能力复用平台」这个定义虽然看起来简单,但经过这么长时间对于中台的实践与思考,我觉得如上文所述的这个定义背后所代表的意义是目前对中台价值的最贴切的阐释:「企业级」定义了中台的范围,区分开了单系统的服务化与微服务;「能力」定义了中台的主要承载对象,能力的抽象解释了各种各样中台的存在;「复用」定义了中台的核心价值,传统的平台化对于易复用性并没有给予足够的关注,中台的提出和兴起,让人们通过可复用性将目光更多的从平台内部转换到平台对于前台业务的支撑上;「平台」定义了中台的主要形式,区别于传统的应用系统拼凑的方式,通过对于更细力度能力的识别与平台化沉淀,实现企业能力的柔性复用,对于前台业务更好的支撑。有了定义后,如何建中台的思路也就豁然开朗:如果说中台是「企业级能力复用平台」的话,那中台化就是「利用平台化手段发现、沉淀与复用企业级能力的过程」。

March 29, 2019 · 1 min · jiezi

jpa 的 save 方法

错误代码如下所示,当时写的时候想着让对象初始化的次数少一点,想着用一个对象。ScoreSummary scoreSummary = new ScoreSummary();// 为每个班级新增成绩汇总for (Klass klass: courseArrangement.getKlassList()) { scoreSummary.setCourseArrangement(courseArrangement); scoreSummary.setKlass(klass); scoreSummaryRepository.save(scoreSummary);}后来潘老师评论说这样只会保存一个对象,之后的会更新。才突然想到这个问题,其实之前是有用到的logger.info(“保存”);CourseArrangement assertCourseArrangement = courseArrangementService.save(courseArrangement);logger.info(“断言保存成功”);assertThat(assertCourseArrangement.getId()).isNotNull();logger.info(“保存”);courseArrangementService.save(courseArrangement);logger.info(“断言保存成功”);assertThat(courseArrangement.getId()).isNotNull();你穿入的对象和你返回的是一个对象。之后看了一下源码,感觉大概应该是我注释的意思。@Transactionalpublic <S extends T> S save(S entity) { if (this.entityInformation.isNew(entity)) { // 判断是否是新建的实体 this.em.persist(entity); // 如果是新增 return entity; } else { return this.em.merge(entity); // 如果不是更新 }}

March 28, 2019 · 1 min · jiezi

Spring Boot 项目打成 war 包部署到 Tomcat

1要知道,Spring Boot 的项目,默认是打为 jar 包的,这时候问题就来了,如果我想打成 war 包部署到 Tomcat,该怎么做呢?又是在网上找了半天的答案,质量不太好,绕来绕去没说个明白。其实还算是非常简单的,只需要大概几个步骤就行了。2首先,在项目的 pom.xml 文件中做一些修改:添加 <packaging>war</packaging>排除掉 web 里面自带的 Tomcat,只需要在spring-boot-starter-web 这个依赖上添加如下内容:添加一个自己的 Tomcat ,在配置文件中,加入下面的依赖即可:添加一个插件,在文件的 build -> plugins 下面添加如下内容:其中需要注意一下,上面的 <warName>ROOT</warName> 表示的是打包之后,war 包的名称,当然你可以改成其他的名字,至于有什么区别,后面再说。3找到项目的启动类,让其继承一个类:SpringBootServletInitializer,并且覆盖 configure 方法,在方法中添加 return builder.sources(WarDemoApplication.class); ,当然,这里的 WarDemoApplication.class 是我的启动类名称,你只需要改成你自己的名称即可。4这时候,进行最后的打包操作了,执行命令 mvn packgae 即可,这时候,war 包就在项目的 target 文件夹下面,因为我是取名为 ROOT 的,所以 war 就叫做 ROOT.war。然后我们可以将 war 包复制到本地的或是远程的 Tomcat 的webapps 目录下面,需要提前删除 webapps 目录下面的所有文件 ,然后启动 Tomcat ,会自动解压这个 war 包。最后,该如何访问项目中的接口呢?如果我部署在了远程的 Tomcat 上面,例如 ip 是 192.168.66.128,那直接访问 192.168.66.128:8080/接口名 ,如果你的 war 包不是以 ROOT 命名,例如叫做 demo.war,那么你的访问路径就是 192.168.66.128:8080/demo/接口名,这也是我上面说到的区别。

March 28, 2019 · 1 min · jiezi

【本人秃顶程序员】Java集合框架综述

←←←←←←←←←←←← 快!点关注一、集合框架图简化图:说明:对于以上的框架图有如下几点说明所有集合类都位于java.util包下。Java的集合类主要由两个接口派生而出:Collection和Map,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。集合接口:6个接口(短虚线表示),表示不同集合类型,是集合框架的基础。抽象类:5个抽象类(长虚线表示),对集合接口的部分实现。可扩展为自定义集合类。实现类:8个实现类(实线表示),对接口的具体实现。Collection 接口是一组允许重复的对象。Set 接口继承 Collection,集合元素不重复。List 接口继承 Collection,允许重复,维护元素插入顺序。Map接口是键-值对象,与Collection接口没有什么关系。Set、List和Map可以看做集合的三大类:List集合是有序集合,集合中的元素可以重复,访问集合中的元素可以根据元素的索引来访问。Set集合是无序集合,集合中的元素不可以重复,访问集合中的元素只能根据元素本身来访问(也是集合里元素不允许重复的原因)。Map集合中保存Key-value对形式的元素,访问时只能根据每项元素的key来访问其value。二、总体分析大致说明:看上面的框架图,先抓住它的主干,即Collection和Map。Collection是一个接口,是高度抽象出来的集合,它包含了集合的基本操作和属性。Collection包含了List和Set两大分支。List是一个有序的队列,每一个元素都有它的索引。第一个元素的索引值是0。List的实现类有LinkedList, ArrayList, Vector, Stack。Set是一个不允许有重复元素的集合。Set的实现类有HastSet和TreeSet。HashSet依赖于HashMap,它实际上是通过HashMap实现的;TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。Map是一个映射接口,即key-value键值对。Map中的每一个元素包含“一个key”和“key对应的value”。AbstractMap是个抽象类,它实现了Map接口中的大部分API。而HashMap,TreeMap,WeakHashMap都是继承于AbstractMap。Hashtable虽然继承于Dictionary,但它实现了Map接口。接下来,再看Iterator。它是遍历集合的工具,即我们通常通过Iterator迭代器来遍历集合。我们说Collection依赖于Iterator,是因为Collection的实现类都要实现iterator()函数,返回一个Iterator对象。ListIterator是专门为遍历List而存在的。再看Enumeration,它是JDK 1.0引入的抽象类。作用和Iterator一样,也是遍历集合;但是Enumeration的功能要比Iterator少。在上面的框图中,Enumeration只能在Hashtable, Vector, Stack中使用。最后,看Arrays和Collections。它们是操作数组、集合的两个工具类。有了上面的整体框架之后,我们接下来对每个类分别进行分析。三、Collection接口Collection接口是处理对象集合的根接口,其中定义了很多对元素进行操作的方法。Collection接口有两个主要的子接口List和Set,注意Map不是Collection的子接口,这个要牢记。Collection接口中的方法如下:其中,有几个比较常用的方法,比如方法add()添加一个元素到集合中,addAll()将指定集合中的所有元素添加到集合中,contains()方法检测集合中是否包含指定的元素,toArray()方法返回一个表示集合的数组。另外,Collection中有一个iterator()函数,它的作用是返回一个Iterator接口。通常,我们通过Iterator迭代器来遍历集合。ListIterator是List接口所特有的,在List接口中,通过ListIterator()返回一个ListIterator对象。Collection接口有两个常用的子接口,下面详细介绍。1.List接口List集合代表一个有序集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List接口继承于Collection接口,它可以定义一个允许重复的有序集合。因为List中的元素是有序的,所以我们可以通过使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。List接口为Collection直接接口。List所代表的是有序的Collection,即它用某种特定的插入顺序来维护元素顺序。用户可以对列表中每个元素的插入位置进行精确地控制,同时可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。(1)ArrayListArrayList是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。size、isEmpty、get、set、iterator和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。ArrayList擅长于随机访问。同时ArrayList是非同步的。(2)LinkedList同样实现List接口的LinkedList与ArrayList不同,ArrayList是一个动态数组,而LinkedList是一个双向链表。所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。由于实现的方式不同,LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。与ArrayList一样,LinkedList也是非同步的。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:List list = Collections.synchronizedList(new LinkedList(…));(3)Vector与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。(4)StackStack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。2.Set接口Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样允许null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。Set接口有三个具体实现类,分别是散列集HashSet、链式散列集LinkedHashSet和树形集TreeSet。Set是一种不包含重复的元素的Collection,无序,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。需要注意的是:虽然Set中元素没有顺序,但是元素在set中的位置是由该元素的HashCode决定的,其具体位置其实是固定的。此外需要说明一点,在set接口中的不重复是有特殊要求的。举一个例子:对象A和对象B,本来是不同的两个对象,正常情况下它们是能够放入到Set里面的,但是如果对象A和B的都重写了hashcode和equals方法,并且重写后的hashcode和equals方法是相同的话。那么A和B是不能同时放入到Set集合中去的,也就是Set集合中的去重和hashcode与equals方法直接相关。为了更好地理解,请看下面的例子:public class Test{ public static void main(String[] args) { Set<String> set=new HashSet<String>(); set.add(“Hello”); set.add(“world”); set.add(“Hello”); System.out.println(“集合的尺寸为:"+set.size()); System.out.println(“集合中的元素为:"+set.toString()); } }运行结果:集合的尺寸为:2集合中的元素为:[world, Hello]分析:由于String类中重写了hashcode和equals方法,用来比较指向的字符串对象所存储的字符串是否相等。所以这里的第二个Hello是加不进去的。再看一个例子:public class TestSet { public static void main(String[] args){ Set<String> books = new HashSet<String>(); //添加一个字符串对象 books.add(new String(“Struts2权威指南”)); //再次添加一个字符串对象, //因为两个字符串对象通过equals方法比较相等,所以添加失败,返回false boolean result = books.add(new String(“Struts2权威指南”)); System.out.println(result); //下面输出看到集合只有一个元素 System.out.println(books); }}运行结果:false[Struts2权威指南]说明:程序中,book集合两次添加的字符串对象明显不是一个对象(程序通过new关键字来创建字符串对象),当使用==运算符判断返回false,使用equals方法比较返回true,所以不能添加到Set集合中,最后只能输出一个元素。(1)HashSetHashSet 是一个没有重复元素的集合。它是由HashMap实现的,不保证元素的顺序(这里所说的没有顺序是指:元素插入的顺序与输出的顺序不一致),而且HashSet允许使用null 元素。HashSet是非同步的,如果多个线程同时访问一个哈希set,而其中至少一个线程修改了该set,那么它必须保持外部同步。 HashSet按Hash算法来存储集合的元素,因此具有很好的存取和查找性能。HashSet的实现方式大致如下,通过一个HashMap存储元素,元素是存放在HashMap的Key中,而Value统一使用一个Object对象。HashSet使用和理解中容易出现的误区:HashSet中存放null值。HashSet中是允许存入null值的,但是在HashSet中仅仅能够存入一个null值。HashSet中存储元素的位置是固定的。HashSet中存储的元素的是无序的,这个没什么好说的,但是由于HashSet底层是基于Hash算法实现的,使用了hashcode,所以HashSet中相应的元素的位置是固定的。必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。(2)LinkedHashSetLinkedHashSet继承自HashSet,其底层是基于LinkedHashMap来实现的,有序,非同步。LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。(3)TreeSetTreeSet是一个有序集合,其底层是基于TreeMap实现的,非线程安全。TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。当我们构造TreeSet时,若使用不带参数的构造函数,则TreeSet的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。注意:TreeSet集合不是通过hashcode和equals函数来比较元素的.它是通过compare或者comparaeTo函数来判断元素是否相等.compare函数通过判断两个对象的id,相同的id判断为重复元素,不会被加入到集合中。四、Map接口Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。1.HashMap以哈希表数据结构实现,查找对象时通过哈希函数计算其位置,它是为快速查询而设计的,其内部定义了一个hash表数组(Entry[] table),元素会通过哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可能通过查看HashMap.Entry的源码它是一个单链表结构。2.LinkedHashMapLinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。由于LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能,但在迭代访问Map里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。3.TreeMapTreeMap 是一个有序的key-value集合,非同步,基于红黑树(Red-Black tree)实现,每一个key-value节点作为红黑树的一个节点。TreeMap存储时会进行排序的,会根据key来对key-value键值对进行排序,其中排序方式也是分为两种,一种是自然排序,一种是定制排序,具体取决于使用的构造方法。自然排序:TreeMap中所有的key必须实现Comparable接口,并且所有的key都应该是同一个类的对象,否则会报ClassCastException异常。定制排序:定义TreeMap时,创建一个comparator对象,该对象对所有的treeMap中所有的key值进行排序,采用定制排序的时候不需要TreeMap中所有的key必须实现Comparable接口。TreeMap判断两个元素相等的标准:两个key通过compareTo()方法返回0,则认为这两个key相等。如果使用自定义的类来作为TreeMap中的key值,且想让TreeMap能够良好的工作,则必须重写自定义类中的equals()方法,TreeMap中判断相等的标准是:两个key通过equals()方法返回为true,并且通过compareTo()方法比较应该返回为0。五、Iterator 与 ListIterator详解1.IteratorIterator的定义如下:public interface Iterator<E> {}Iterator是一个接口,它是集合的迭代器。集合可以通过Iterator去遍历集合中的元素。Iterator提供的API接口如下:boolean hasNext():判断集合里是否存在下一个元素。如果有,hasNext()方法返回 true。Object next():返回集合里下一个元素。void remove():删除集合里上一次next方法返回的元素。使用示例:public class IteratorExample { public static void main(String[] args) { ArrayList<String> a = new ArrayList<String>(); a.add(“aaa”); a.add(“bbb”); a.add(“ccc”); System.out.println(“Before iterate : " + a); Iterator<String> it = a.iterator(); while (it.hasNext()) { String t = it.next(); if (“bbb”.equals(t)) { it.remove(); } } System.out.println(“After iterate : " + a); }}输出结果如下:Before iterate : [aaa, bbb, ccc]After iterate : [aaa, ccc]注意:Iterator只能单向移动。Iterator.remove()是唯一安全的方式来在迭代过程中修改集合;如果在迭代过程中以任何其它的方式修改了基本集合将会产生未知的行为。而且每调用一次next()方法,remove()方法只能被调用一次,如果违反这个规则将抛出一个异常。2.ListIteratorListIterator是一个功能更加强大的迭代器, 它继承于Iterator接口,只能用于各种List类型的访问。可以通过调用listIterator()方法产生一个指向List开始处的ListIterator, 还可以调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。ListIterator接口定义如下:public interface ListIterator<E> extends Iterator<E> { boolean hasNext(); E next(); boolean hasPrevious(); E previous(); int nextIndex(); int previousIndex(); void remove(); void set(E e); void add(E e);}由以上定义我们可以推出ListIterator可以:双向移动(向前/向后遍历).产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引.可以使用set()方法替换它访问过的最后一个元素.可以使用add()方法在next()方法返回的元素之前或previous()方法返回的元素之后插入一个元素.使用示例:public class ListIteratorExample { public static void main(String[] args) { ArrayList<String> a = new ArrayList<String>(); a.add(“aaa”); a.add(“bbb”); a.add(“ccc”); System.out.println(“Before iterate : " + a); ListIterator<String> it = a.listIterator(); while (it.hasNext()) { System.out.println(it.next() + “, " + it.previousIndex() + “, " + it.nextIndex()); } while (it.hasPrevious()) { System.out.print(it.previous() + " “); } System.out.println(); it = a.listIterator(1); while (it.hasNext()) { String t = it.next(); System.out.println(t); if (“ccc”.equals(t)) { it.set(“nnn”); } else { it.add(“kkk”); } } System.out.println(“After iterate : " + a); }}输出结果如下:Before iterate : [aaa, bbb, ccc]aaa, 0, 1bbb, 1, 2ccc, 2, 3ccc bbb aaa bbbcccAfter iterate : [aaa, bbb, kkk, nnn]六、异同点1.ArrayList和LinkedListArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。2.HashTable与HashMap相同点:都实现了Map、Cloneable、java.io.Serializable接口。都是存储"键值对(key-value)“的散列表,而且都是采用拉链法实现的。不同点:历史原因:HashTable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现 。同步性:HashTable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的 。对null值的处理:HashMap的key、value都可为null,HashTable的key、value都不可为null 。基类不同:HashMap继承于AbstractMap,而Hashtable继承于Dictionary。Dictionary是一个抽象类,它直接继承于Object类,没有实现任何接口。Dictionary类是JDK 1.0的引入的。虽然Dictionary也支持“添加key-value键值对”、“获取value”、“获取大小”等基本操作,但它的API函数比Map少;而且Dictionary一般是通过Enumeration(枚举类)去遍历,Map则是通过Iterator(迭代M器)去遍历。 然而由于Hashtable也实现了Map接口,所以,它即支持Enumeration遍历,也支持Iterator遍历。AbstractMap是一个抽象类,它实现了Map接口的绝大部分API函数;为Map的具体实现类提供了极大的便利。它是JDK 1.2新增的类。支持的遍历种类不同:HashMap只支持Iterator(迭代器)遍历。而Hashtable支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历。3.HashMap、Hashtable、LinkedHashMap和TreeMap比较Hashmap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。HashMap最多只允许一条记录的键为Null;允许多条记录的值为Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力。Hashtable 与 HashMap类似,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢。LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。对于LinkedHashMap而言,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。TreeMap实现SortMap接口,内部实现是红黑树。能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。TreeMap不允许key的值为null。非同步的。一般情况下,我们用的最多的是HashMap,HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map 中插入、删除和定位元素,HashMap 是最好的选择。TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。import java.util.HashMap;import java.util.Iterator;import java.util.LinkedHashMap;import java.util.TreeMap;public class MapTest { public static void main(String[] args) { //HashMap HashMap<String,String> hashMap = new HashMap(); hashMap.put(“4”, “d”); hashMap.put(“3”, “c”); hashMap.put(“2”, “b”); hashMap.put(“1”, “a”); Iterator<String> iteratorHashMap = hashMap.keySet().iterator(); System.out.println(“HashMap–>”); while (iteratorHashMap.hasNext()){ Object key1 = iteratorHashMap.next(); System.out.println(key1 + “–” + hashMap.get(key1)); } //LinkedHashMap LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap(); linkedHashMap.put(“4”, “d”); linkedHashMap.put(“3”, “c”); linkedHashMap.put(“2”, “b”); linkedHashMap.put(“1”, “a”); Iterator<String> iteratorLinkedHashMap = linkedHashMap.keySet().iterator(); System.out.println(“LinkedHashMap–>”); while (iteratorLinkedHashMap.hasNext()){ Object key2 = iteratorLinkedHashMap.next(); System.out.println(key2 + “–” + linkedHashMap.get(key2)); } //TreeMap TreeMap<String,String> treeMap = new TreeMap(); treeMap.put(“4”, “d”); treeMap.put(“3”, “c”); treeMap.put(“2”, “b”); treeMap.put(“1”, “a”); Iterator<String> iteratorTreeMap = treeMap.keySet().iterator(); System.out.println(“TreeMap–>”); while (iteratorTreeMap.hasNext()){ Object key3 = iteratorTreeMap.next(); System.out.println(key3 + “–” + treeMap.get(key3)); } }}输出结果:HashMap–>3–c2–b1–a4–dLinkedHashMap–>4–d3–c2–b1–aTreeMap–>1–a2–b3–c4–d4.HashSet、LinkedHashSet、TreeSet比较Set接口Set不允许包含相同的元素,如果试图把两个相同元素加入同一个集合中,add方法返回false。Set判断两个对象相同不是使用==运算符,而是根据equals方法。也就是说,只要两个对象用equals方法比较返回true,Set就不会接受这两个对象。HashSetHashSet有以下特点:不能保证元素的排列顺序,顺序有可能发生变化。不是同步的。集合元素可以是null,但只能放入一个null。当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值也相等。注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算 hashCode的值。LinkedHashSetLinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。TreeSet类TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0。自然排序自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是负数,则表明obj1小于obj2。如果我们将两个对象的equals方法总是返回true,则这两个对象的compareTo方法返回应该返回0。定制排序自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法。package com.test; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.TreeSet; /** * @description 几个set的比较 * HashSet:哈希表是通过使用称为散列法的机制来存储信息的,元素并没有以某种特定顺序来存放; * LinkedHashSet:以元素插入的顺序来维护集合的链接表,允许以插入的顺序在集合中迭代; * TreeSet:提供一个使用树结构存储Set接口的实现,对象以升序顺序存储,访问和遍历的时间很快。 * @author Zhou-Jingxian * */ public class SetDemo { public static void main(String[] args) { HashSet<String> hs = new HashSet<String>(); hs.add(“B”); hs.add(“A”); hs.add(“D”); hs.add(“E”); hs.add(“C”); hs.add(“F”); System.out.println(“HashSet 顺序:\n”+hs); LinkedHashSet<String> lhs = new LinkedHashSet<String>(); lhs.add(“B”); lhs.add(“A”); lhs.add(“D”); lhs.add(“E”); lhs.add(“C”); lhs.add(“F”); System.out.println(“LinkedHashSet 顺序:\n”+lhs); TreeSet<String> ts = new TreeSet<String>(); ts.add(“B”); ts.add(“A”); ts.add(“D”); ts.add(“E”); ts.add(“C”); ts.add(“F”); System.out.println(“TreeSet 顺序:\n”+ts); } }输出结果:HashSet 顺序:[D, E, F, A, B, C]LinkedHashSet 顺序:[B, A, D, E, C, F]TreeSet 顺序:[A, B, C, D, E, F]5、Iterator和ListIterator区别我们在使用List,Set的时候,为了实现对其数据的遍历,我们经常使用到了Iterator(迭代器)。使用迭代器,你不需要干涉其遍历的过程,只需要每次取出一个你想要的数据进行处理就可以了。但是在使用的时候也是有不同的。List和Set都有iterator()来取得其迭代器。对List来说,你也可以通过listIterator()取得其迭代器,两种迭代器在有些时候是不能通用的,Iterator和ListIterator主要区别在以下方面:ListIterator有add()方法,可以向List中添加对象,而Iterator不能ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。其实,数组对象也可以用迭代器来实现。6、Collection 和 Collections区别(1)java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set (2)java.util.Collections 是一个包装类(工具类/帮助类)。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,用于对集合中元素进行排序、搜索以及线程安全等各种操作,服务于Java的Collection框架。代码示例:import java.util.ArrayList; import java.util.Collections; import java.util.List; public class TestCollections { public static void main(String args[]) { //注意List是实现Collection接口的 List list = new ArrayList(); double array[] = { 112, 111, 23, 456, 231 }; for (int i = 0; i < array.length; i++) { list.add(new Double(array[i])); } Collections.sort(list); for (int i = 0; i < array.length; i++) { System.out.println(list.get(i)); } // 结果:23.0 111.0 112.0 231.0 456.0 } } ...

March 28, 2019 · 3 min · jiezi

Spring-RestTemplate之urlencode参数解析异常全程分析

对接外部的一个接口时,发现一个鬼畜的问题,一直提示缺少某个参数,同样的url,通过curl命令访问ok,但是改成RestTemplate请求就不行;因为提供接口的是外部的,所以也无法从服务端着手定位问题,特此记录下这个问题的定位以及解决过程<!– more –>I. 问题复现首先我们是通过get请求访问服务端,参数直接拼接在url中;与我们常规的get请求有点不一样的是其中一个参数要求url编码之后传过去。因为不知道服务端的实现,所以再事后定位到这个问题之后,反推了一个服务端可能实现方式1. web服务模拟模拟一个接口,要求必须传入accessKey,且这个参数必须和我们定义的一样(模拟身份标志,用户请求必须带上自己的accessKey, 且必须合法)@RestControllerpublic class HelloRest { public final String ALLOW_KEY = “ASHJRK3LJFD+R32SADFLK+FASDJ=”; @GetMapping(path = “access”) public String access(String accessKey, String name) { System.out.println(accessKey + “|” + name) ; if (ALLOW_KEY.equals(accessKey)) { return “true”; } else { return “false”; } }}这个接口只支持get请求,把参数放在url中的时候,很明显这个accessKey需要编码2. 访问验证在拼接访问url时,首先对accessKey进行编码,得到一个访问的连接 http://localhost:39531/access?accessKey=ASHJRK3LJFD%2BR32SADFLK%2BFASDJ%3D&name=yihuihui下面看下浏览器 + curl + restTemplate三种访问姿势的返回结果浏览器访问结果:curl访问结果:restTemplate访问结果:@Testpublic void testUrlEncode() { String url = “http://localhost:39531/access?accessKey=ASHJRK3LJFD%2BR32SADFLK%2BFASDJ%3D&name=yihuihui”; RestTemplate restTemplate = new RestTemplate(); String ans = restTemplate.getForObject(url, String.class); System.out.println(ans);}看到上面的输出,结果就很有意思了,同样的url为啥前面的访问没啥问题,换到RestTemplate就不对了???II. 问题定位分析如果服务端的代码也在我们的掌控中,可以通过debug服务端,查看请求参数来定位问题;但是这个问题出现时,服务端不在掌握中,这个时候就只能从客户端出发,来推测可能出现问题的原因了;接下来记录下我们定位这个问题的"盲人摸象"过程1. 问题猜测很容易怀疑问题出在url编码后的参数上,直接传这种编码后的url参数会不会解析有问题,既然编码之后不行,那就改成不编码试一试@Testpublic void testUrlEncode() { String url = “http://localhost:39531/access?accessKey=ASHJRK3LJFD%2BR32SADFLK%2BFASDJ%3D&name=yihuihui”; RestTemplate restTemplate = new RestTemplate(); String ans = restTemplate.getForObject(url, String.class); System.out.println(ans); url = “http://localhost:39531/access?accessKey=ASHJRK3LJFD+R32SADFLK+FASDJ=&name=yihuihui”; ans = restTemplate.getForObject(url, String.class); System.out.println(ans);}毫无疑问,访问依然失败,模拟case如下传编码后的不行,传编码之前的也不行,这就蛋疼了;接下来怎么办?换个http包试一试接下来改用HttpClient访问,看下能不能正常访问@Testpublic void testUrlEncode() throws IOException { String url = “http://localhost:39531/access?accessKey=ASHJRK3LJFD%2BR32SADFLK%2BFASDJ%3D&name=yihuihui”; RestTemplate restTemplate = new RestTemplate(); String ans = restTemplate.getForObject(url, String.class); System.out.println(ans); //创建httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); //创建请求方法的实例, 并指定请求url HttpGet httpget = new HttpGet(url); //获取http响应状态码 CloseableHttpResponse response = httpClient.execute(httpget); HttpEntity entity = response.getEntity(); //接收响应头 String content = EntityUtils.toString(entity, “utf-8”); System.out.println(httpget.getURI()); System.out.println(content); httpClient.close();}输出结果如下,神器的一幕出现了,返回结果正常了到了这一步,基本上可以知道是RestTemplate的使用问题了,要么就是操作姿势不对,要么就是RestTemplate有什么潜规则是我们不知道的2. 问题定位同样的url,两种不同的包返回结果不一样,自然而然的就会想到对比下两个的实现方式了,看看哪里不同;如果对两个包的源码不太熟悉的话,想一下子定位都问题,并不容易,对这两个源码,我也是不熟的,不过因为巧和,没有深入到底层的实现就发现了疑是问题的关键点所在首先看的RestTemplate的发起请求的逻辑,如下(下图中有关键点,单独看不太容易抓到)接下来再去debug HttpClient的请求链路中,在创建HttpGet对象时,看到下面这一行代码单独看上面两个,好像发现不了什么问题;但是两个对比着看,就发现一个有意思的地方了,在HttpTemplate的execute方法中,创建URI居然不是我们熟知的 URI.create(),接下来就来验证下是不是这里的问题了;测试方法也比较简单,直接传入URI对象参数,看能否访问成功@Testpublic void testUrlEncode() throws IOException { String url = “http://localhost:39531/access?accessKey=ASHJRK3LJFD%2BR32SADFLK%2BFASDJ%3D&name=yihuihui”; RestTemplate restTemplate = new RestTemplate(); String ans = restTemplate.getForObject(url, String.class); System.out.println(ans); ans = restTemplate.getForObject(URI.create(url), String.class); System.out.println(ans);}从截图也可以看出,返回true表示成功了,因此我们可以圈定问题的范围,就在RestTemplate中url参数的构建上了3. 原因分析前面定位到了出问题的环节,在RestTemplate创建URI对象的地方,接下来我们深入源码,看一下这段逻辑的神奇之处通过单步执行,下面截取关键链路的代码,下面圈出的就是定位最终实现uri创建的具体对象org.springframework.web.util.DefaultUriBuilderFactory.DefaultUriBuilder接下来重点放在具体实现方法中// org.springframework.web.util.DefaultUriBuilderFactory.DefaultUriBuilder#build(java.lang.Object…)@Overridepublic URI build(Map<String, ?> uriVars) { if (!defaultUriVariables.isEmpty()) { Map<String, Object> map = new HashMap<>(); map.putAll(defaultUriVariables); map.putAll(uriVars); uriVars = map; } if (encodingMode.equals(EncodingMode.VALUES_ONLY)) { uriVars = UriUtils.encodeUriVariables(uriVars); } UriComponents uriComponents = this.uriComponentsBuilder.build().expand(uriVars); if (encodingMode.equals(EncodingMode.URI_COMPONENT)) { uriComponents = uriComponents.encode(); } return URI.create(uriComponents.toString());}@Overridepublic URI build(Object… uriVars) { if (ObjectUtils.isEmpty(uriVars) && !defaultUriVariables.isEmpty()) { return build(Collections.emptyMap()); } if (encodingMode.equals(EncodingMode.VALUES_ONLY)) { uriVars = UriUtils.encodeUriVariables(uriVars); } UriComponents uriComponents = this.uriComponentsBuilder.build().expand(uriVars); if (encodingMode.equals(EncodingMode.URI_COMPONENT)) { uriComponents = uriComponents.encode(); } return URI.create(uriComponents.toString());}两个builder方法提供关键URI生成逻辑,根据最后的返回可以知道,生成URI依然是使用URI.create,所以出问题的地方就应该是 uriComponents.encode() 实现url编码的地方了,对应的代码如下// org.springframework.web.util.HierarchicalUriComponents#encode@Overridepublic HierarchicalUriComponents encode(Charset charset) { if (this.encoded) { return this; } String scheme = getScheme(); String fragment = getFragment(); String schemeTo = (scheme != null ? encodeUriComponent(scheme, charset, Type.SCHEME) : null); String fragmentTo = (fragment != null ? encodeUriComponent(fragment, charset, Type.FRAGMENT) : null); String userInfoTo = (this.userInfo != null ? encodeUriComponent(this.userInfo, charset, Type.USER_INFO) : null); String hostTo = (this.host != null ? encodeUriComponent(this.host, charset, getHostType()) : null); PathComponent pathTo = this.path.encode(charset); MultiValueMap<String, String> paramsTo = encodeQueryParams(charset); return new HierarchicalUriComponents( schemeTo, fragmentTo, userInfoTo, hostTo, this.port, pathTo, paramsTo, true, false);}// org.springframework.web.util.HierarchicalUriComponents#encodeQueryParamsprivate MultiValueMap<String, String> encodeQueryParams(Charset charset) { int size = this.queryParams.size(); MultiValueMap<String, String> result = new LinkedMultiValueMap<>(size); this.queryParams.forEach((key, values) -> { String name = encodeUriComponent(key, charset, Type.QUERY_PARAM); List<String> encodedValues = new ArrayList<>(values.size()); for (String value : values) { encodedValues.add(encodeUriComponent(value, charset, Type.QUERY_PARAM)); } result.put(name, encodedValues); }); return result;}记录下参数编码的前后对比,编码前参数为 ASHJRK3LJFD%2BR32SADFLK%2BFASDJ%3D编码之后,参数变为ASHJRK3LJFD%252BR32SADFLK%252BFASDJ%253D对比下上面的区别,发现这个参数编码,会将请求参数中的 % 编码为 %25, 所以问题就清楚了,我传进来本来就已经是编码之后的了,结果再编码一次,相当于修改了请求参数了看到这里,自然而然就有一个想法,既然你会给我的参数进行编码,那么为啥我传入的非编码的参数也不行呢?接下来我们改一下请求的url参数,再执行一下上面的过程,看下编码之后的参数长啥样从上图很明显可以看出,现编码之后的和我们URLEncode的结果不一样,加号没有被编码, 我们调用jdk的url解码,发现将上面编码后的内容解码出来,+号没了所以问题的原因也找到了,RestTemplate中首先url编码解码的逻辑和URLEncode/URLDecode不一致导致的4. 关键代码分析最后一步,就是看下具体的url参数编码的实现方法了,下面贴出源码,并在关键地方给出说明// org.springframework.web.util.HierarchicalUriComponents#encodeUriComponent(java.lang.String, java.nio.charset.Charset, org.springframework.web.util.HierarchicalUriComponents.Type)static String encodeUriComponent(String source, Charset charset, Type type) { if (!StringUtils.hasLength(source)) { return source; } Assert.notNull(charset, “Charset must not be null”); Assert.notNull(type, “Type must not be null”); byte[] bytes = source.getBytes(charset); ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length); boolean changed = false; for (byte b : bytes) { if (b < 0) { b += 256; } // 注意这一行,我们的type实际上为 org.springframework.web.util.HierarchicalUriComponents.Type#QUERY_PARAM if (type.isAllowed(b)) { bos.write(b); } else { bos.write(’%’); char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); bos.write(hex1); bos.write(hex2); changed = true; } } return (changed ? new String(bos.toByteArray(), charset) : source);}if/else 这一段逻辑需要捞出来好好看一下,这里决定了什么字符会进行编码;其中 type.isAllowed 对应的代码为// org.springframework.web.util.HierarchicalUriComponents.Type#QUERY_PARAMQUERY_PARAM { @Override public boolean isAllowed(int c) { if (’=’ == c || ‘&’ == c) { return false; } else { return isPchar(c) || ‘/’ == c || ‘?’ == c; } }},// isPchar 对应的相关代码为/** * Indicates whether the given character is in the {@code pchar} set. * @see <a href=“http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a> /protected boolean isPchar(int c) { return (isUnreserved(c) || isSubDelimiter(c) || ‘:’ == c || ‘@’ == c);}/* * Indicates whether the given character is in the {@code unreserved} set. * @see <a href=“http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a> /protected boolean isUnreserved(int c) { return (isAlpha(c) || isDigit(c) || ‘-’ == c || ‘.’ == c || ‘_’ == c || ‘~’ == c);}/* * Indicates whether the given character is in the {@code sub-delims} set. * @see <a href=“http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a> /protected boolean isSubDelimiter(int c) { return (’!’ == c || ‘$’ == c || ‘&’ == c || ‘'’ == c || ‘(’ == c || ‘)’ == c || ‘’ == c || ‘+’ == c || ‘,’ == c || ‘;’ == c || ‘=’ == c);}/** * Indicates whether the given character is in the {@code ALPHA} set. * @see <a href=“http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a> /protected boolean isAlpha(int c) { return (c >= ‘a’ && c <= ‘z’ || c >= ‘A’ && c <= ‘Z’);}/* * Indicates whether the given character is in the {@code DIGIT} set. * @see <a href=“http://www.ietf.org/rfc/rfc3986.txt">RFC 3986, appendix A</a> */protected boolean isDigit(int c) { return (c >= ‘0’ && c <= ‘9’);}上面涉及的方法挺多,小结一下需要转码的字符为: =, &下图是维基百科中关于url参数编码的说明,比如上例中的+号,按照维基百科的需要转码;但是在Spring中却是不需要转码的所以为啥Spring要这么干呢?网上搜索了一下,发现有人也遇到过这个问题,并提给了Spring的官方,对应链接为HierarchicalUriComponents.encodeUriComponent() method can not encode Pchar官方人员的解释如下根据 RFC 3986 加号等符号的确实可以出现在参数中的,而且不需要编码,有问题的在于服务端的解析没有与时俱进III. 小结最后复盘一下这个问题,当使用RestTemplate发起请求时,如果请求参数中有需要url编码时,不希望出现问题的使用姿势应传入URI对象而不是字符串,如下面两种方式@Override@Nullablepublic <T> T execute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException { return doExecute(url, method, requestCallback, responseExtractor);}@Override@Nullablepublic <T> T getForObject(URI url, Class<T> responseType) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor);}注意Spring的url参数编码,默认只会针对 = 和 & 进行处理;为了兼容我们一般的后端的url编解码处理在需要编码参数时,目前尽量不要使用Spring默认的方式,不然接收到数据会和预期的不一致IV. 其他0. 项目工程:spring-boot-demo1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

March 27, 2019 · 4 min · jiezi

Spring事件机制源码分析

前言由于之前使用Spring事件机制出现了问题,所以特意去了解这块的源码。Spring事件机制其实就是事件发布/订阅(注意在Spring中订阅指的是监听)。PS:Spring版本为5.1.5.RELEASE源码分析初始化初始化这块关键是核心组件的注册ApplicationEventPublisher的初始化与注册,关键方法为AbstractApplicationContext的方法prepareBeanFactory()ApplicationEventMulticaster的初始化与注册,关键方法为AbstractApplicationContext的initApplicationEventMulticaster()方法ApplicationListener的初始化与注册,关键方法为AbstractApplicationContext的registerListeners()方法这块不细说,感兴趣的可以自行跟踪关键方法事件发布/订阅事件发布/订阅的关键方法为AbstractApplicationContext的publishEvent,源码如下: protected void publishEvent(Object event, ResolvableType eventType) { // 避免空指针 Assert.notNull(event, “Event must not be null”); if (logger.isTraceEnabled()) { logger.trace(“Publishing event in " + getDisplayName() + “: " + event); } // 处理event对象,将其转换为ApplicationEvent ApplicationEvent applicationEvent; if (event instanceof ApplicationEvent) { applicationEvent = (ApplicationEvent) event; } else { applicationEvent = new PayloadApplicationEvent<Object>(this, event); if (eventType == null) { eventType = ((PayloadApplicationEvent) applicationEvent).getResolvableType(); } } // 是否延迟多播,即将事件发布到所有监听器中 if (this.earlyApplicationEvents != null) { this.earlyApplicationEvents.add(applicationEvent); } else { //此处为事件监听处理器的调用关键 getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType); } // 是否将事件发布到父容器中 if (this.parent != null) { if (this.parent instanceof AbstractApplicationContext) { ((AbstractApplicationContext) this.parent).publishEvent(event, eventType); } else { this.parent.publishEvent(event); } } }通过代码跟踪,发现Spring中使用ApplicationEventMulticaster的默认实现SimpleApplicationEventMulticaster来触发事件的监听,关键方法为multicastEvent()方法,源码如下: @Override public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) { // 获取事件类型 ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {//依次遍历事件监听器 // 获取线程池 Executor executor = getTaskExecutor(); if (executor != null) {//线程池不为null,则异步调用监听器 executor.execute(new Runnable() { @Override public void run() { invokeListener(listener, event); } }); } else {// 同步调用监听器 invokeListener(listener, event); } } } ...

March 27, 2019 · 1 min · jiezi

Spring事件机制问题排查

前言之前使用Spring的事件机制来改造系统,完成了部分模块的解耦。但是实际使用时却发现存在以下问题:当ApplicationEventPublisher批量推送ApplicationEvent时,如果ApplicationListener在处理的过程中抛出异常,则会导致后续的推送中断。PS:Spring版本为5.1.5.RELEASE下面将会展示一个复盘的示例复盘示例自定义事件import org.springframework.context.ApplicationEvent;/** * 自定义事件 * @author RJH * create at 2018/10/29 /public class SimpleEvent extends ApplicationEvent { private int i; /* * Create a new ApplicationEvent. * * @param source the object on which the event initially occurred (never {@code null}) / public SimpleEvent(Object source) { super(source); i=Integer.valueOf(source.toString()); } public int getI() { return i; }}事件监听器import org.springframework.context.ApplicationListener;import org.springframework.stereotype.Component;/* * 自定义事件监听器 * @author RJH * create at 2018/10/29 /@Componentpublic class SimpleEventListener implements ApplicationListener<SimpleEvent> { @Override public void onApplicationEvent(SimpleEvent event) { if(event.getI()%10==0){ throw new RuntimeException(); } System.out.println(“Time:"+event.getTimestamp()+” event:"+event.getSource()); }}事件推送import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;/* * 事件推送 * @author RJH * create at 2018/10/29 /public class EventApplication { public static void main(String[] args) { //扫描特定package ApplicationContext context=new AnnotationConfigApplicationContext(“com.rjh.event”); for(int i=1;i<=100;i++){//批量推送事件 context.publishEvent(new SimpleEvent(i)); } }}运行结果Time:1553607971143 event:1Time:1553607971145 event:2Time:1553607971145 event:3Time:1553607971145 event:4Time:1553607971145 event:5Time:1553607971145 event:6Time:1553607971146 event:7Time:1553607971146 event:8Time:1553607971146 event:9Exception in thread “main” java.lang.RuntimeException at com.rjh.event.SimpleEventListener.onApplicationEvent(SimpleEventListener.java:17) at com.rjh.event.SimpleEventListener.onApplicationEvent(SimpleEventListener.java:11) at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172) at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165) at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139) at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:393) at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:347) at com.rjh.event.EventApplication.main(EventApplication.java:17)分析期待结果为SimpleEventListener抛出异常不影响EventApplication中后续事件的推送。但是实际上却是SimpleEventListener抛出异常会导致EventApplication后续事件的推送中断。从这里可以看出事件的推送和事件的监听是同步阻塞进行,而并非是异步。详细可以参考文档中的介绍:Notice that ApplicationListener is generically parameterized with the type of your custom event (BlackListEvent in the preceding example). This means that the onApplicationEvent() method can remain type-safe, avoiding any need for downcasting. You can register as many event listeners as you wish, but note that, by default, event listeners receive events synchronously. This means that the publishEvent() method blocks until all listeners have finished processing the event. One advantage of this synchronous and single-threaded approach is that, when a listener receives an event, it operates inside the transaction context of the publisher if a transaction context is available. If another strategy for event publication becomes necessary, See the javadoc for Spring’s ApplicationEventMulticaster interface.解决办法将事件监听改造为异步处理,这里将会展示基于JavaConfig即注解的解决方案开启异步import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableAsync;/* * 开启异步服务配置类 * @author RJH * create at 2019-03-26 /@EnableAsync@Configurationpublic class AsyncConfig {}异步事件监听import org.springframework.context.event.EventListener;import org.springframework.scheduling.annotation.Async;import org.springframework.stereotype.Component;/* * 异步事件监听 * @author RJH * create at 2019-03-26 */@Componentpublic class AsyncSimpleEventListener { @EventListener @Async public void handleEvent(SimpleEvent event){ if(event.getI()%10==0){ throw new RuntimeException(); } System.out.println(“Time:"+event.getTimestamp()+” event:"+event.getSource()); }}运行结果Time:1553614469990 event:1Time:1553614470007 event:72Time:1553614470006 event:64Time:1553614470006 event:67Time:1553614470007 event:73Time:1553614470007 event:71Time:1553614470007 event:75Time:1553614470006 event:68Time:1553614470007 event:69Time:1553614470006 event:62Time:1553614470005 event:61Time:1553614470006 event:63Time:1553614470006 event:65Time:1553614470007 event:74Time:1553614470006 event:66Time:1553614470005 event:59Time:1553614470005 event:57Time:1553614470005 event:55Time:1553614470005 event:58Time:1553614470004 event:51Time:1553614470004 event:52Time:1553614470002 event:43Time:1553614470004 event:53Time:1553614470002 event:38Time:1553614470001 event:36Time:1553614470004 event:54Time:1553614470001 event:33Time:1553614470000 event:29Time:1553614470000 event:27Time:1553614470005 event:56Time:1553614469999 event:23Time:1553614469999 event:22Time:1553614469999 event:21三月 26, 2019 11:34:30 下午 org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler handleUncaughtException严重: Unexpected error occurred invoking async method: public void com.rjh.event.AsyncSimpleEventListener.handleEvent(com.rjh.event.SimpleEvent)Time:1553614470000 event:24java.lang.RuntimeException at com.rjh.event.AsyncSimpleEventListener.handleEvent(AsyncSimpleEventListener.java:19) at com.rjh.event.AsyncSimpleEventListener$$FastClassBySpringCGLIB$$61742dbf.invoke(<generated>)Time:1553614469998 event:15 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:736) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:115) at java.util.concurrent.FutureTask.run(FutureTask.java:266)…内容过长省略部分结果分析改造为异步执行后,事件监听就由线程池进行处理,此处还可以通过自定义线程池,并设置异常处理器来处理未捕获的异常。参考资料https://docs.spring.io/spring…https://docs.spring.io/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

开工大吉!简单的说说公司的开发规范!

大家好,好久没有写公众号了,最近有朋友参加面试被问到开发规范的问题,突然发现每天干着工作,却没有关注这个问题,就想着写篇文章,简单的说下自己公司的开发规范。关于规范,每个公司都有自己独特的开发规范,归根结底,好的规范才能提高一个团队的效率,接下来,简单的说下自己公司的开发规范,如果大家能在其中有所收获,就是值得的,欢迎评论区交流。接口规范:1、在开发之前必须要先定义接口,定义接口就必须要思考你的需求,逻辑,在写接口文档的时候其实你就已经在你的大脑中实现了一遍你的需求了。2、你定义的接口也是要有标准的,包括不包含多余的字段,正式环境和测试环境的数据格式必须一致,文档与真实开发出来的接口必须一致等等。3、在开发的过程中,如果接口有变化,需要及时和前端或者客户端沟通,避免因为信息的不同步问题而导致工期延误。4、还有前端和APP拿到你的接口数据之后不需要再次的进行逻辑处理,比如说,状态字段是int类型,你把所有的枚举类型给他,让他自己去循环判断应该显示哪个中文,如果接口定义成这样,那这个接口就是不太合格的,你可以在接口返回数据中添加一个字段来避免使用者的多余的工作量。上线规范:1、首先在开发完成后,我们需要自测,自测的标准并不是特别的高,只需要通过冒烟测试,能够把正常的流程走通就可以了,千万不要自测还没测好就交给测试,当测试辛辛苦苦的录完数据,走正常的流程的时候报个系统异常,这种心情应该是十分酸爽的。只有当这些常规的测试走通的时候,测试才会给你测那些比较不容易发现的问题,如果测试总是在这些显而易见的问题上兜兜转转,那么在有限的时间内,测出的产品可能质量也并不高。2、其次测试通过之后,关注下在正式环境上是否需要资源申请,比如说服务器,redis,数据库,这些东西需要提前的给运维提交工单,让运维能够从容不迫的去准备,避免在上线那天因为资源还没准备好而耽误太长的时间。3、在测试通过,运维准备好资源的时候,就可以部署到线上了,我们的代码现在应该是在dev分支上,我们需要把代码合并到master分支上(这里需要说明下,master分支上千万不要修改代码,我们要时刻保证master分支上的代码是和线上环境保持一致的),之后就可以通过Jenkins或者其他部署工具部署项目了。4、部署之后,我们不能直接通知测试来测试了,我们需要用我们的测试用例,自己先访问下我们的正式环境的接口,看下是否正常,之后在通知测试回测。等待着测试汇报答复(每次上线听到测试说没有问题,心里豁然开朗)上线完成。这里说下,在线上部署的同时需要注意的点,在dev和master分支合并代码之后要进行代码review,避免自己的误操作带来不必要的问题。当在正式环境遇到问题的时候,我们需要先通过自己的测试用例来定位问题,可以单点线上tomcat来确定服务是否存在代码问题,如果是代码问题,修改后第二次合并代码的时候要慎重,可以使用交叉review的方式。如果问题归属配置问题,及时找运维沟通解决。上线完成后,要对master分支上打tag,在tag中说明此次部署上线的主要内容。以上只是简单的说了下接口文档和上线的规范,接下来还会说数据库设计相关的规范,作为自己的知识总结,也希望能帮助到其他人。这里会长期的分享技术干货、日常工作总结与思考,你的点赞和分享是对我最大的支持,感谢。如果这篇文章让你有所收获,欢迎关注公众号 java技术情报局

March 26, 2019 · 1 min · jiezi

mica cglib 增强——[1]cglib bean copy 介绍

专栏介绍本套专栏主要是介绍微服务核心框架 Mica 中对 Cglib bean copy 的一系列增强,保证高性能的同时,提高易用性。整个专栏有 6 篇文章,感兴趣的朋友请加关注。专栏目录cglib bean copy 介绍。mica bean copy 介绍和链式 bean copy 的支持。mica bean 支持 copy 原始类型和封装类型。mica bean 支持 copy map 到 bean。使用Spring的类型转换增强 mica bean copy。mica bean、Map 互转增强和总结。Cglib BeanCopier 介绍阿里巴巴 p3c 插件中有这么一项检查 “避免用Apache Beanutils进行属性的copy,Apache BeanUtils性能较差,可以使用其他方案比如Spring BeanUtils, Cglib BeanCopier”。今天我们的主角主要就是 Cglib 的 BeanCopier。性能下图是 github 上的一个 Bean copy 性能的对比,可以看出 Bean copy 工具性能差距还是比较大。更多请见:https://github.com/yangtu222/BeanUtils#performance图中可以看出,Cglib BeanCopier 的性能十分强劲,也难怪阿里巴巴规范中也推荐,下面我们来看看它具体的使用方式。使用Cglib 以源码的形式纳入到 Spring core 中,所有大家使用 Spring、Spring boot 可以直接使用。其它则需要自己添加依赖,下面的使用例子都以 Spring 的为主。注意:使用了 Lombok 。User 对象@Datapublic class User { private Integer id; private String name; private Integer age;}UserVo 对象@Datapublic class UserVo { private String name; private Integer age;}Bean 拷贝import org.springframework.cglib.beans.BeanCopier;public class UserCopyTest { public static void main(String[] args) { // 1. 初始化 user,赋值 User user = new User(); user.setId(250); user.setName(“如梦技术”); user.setAge(30); // 2. 初始化 userVo UserVo userVo = new UserVo(); // 3. 构造 BeanCopier,不是用类型转换 BeanCopier copier = BeanCopier.create(User.class, UserVo.class, false); // 4. 拷贝对象,不是用类型转换,转换器可以使用 null copier.copy(user, userVo, null); // 5. 打印结果:UserVo(name=如梦技术, age=30) System.out.println(userVo); }}原理大家都知道 Cglib BeanCopier,之所以性能这么高主要是利用了 Asm 字节码技术。在 UserCopyTest 的 main 方法中添加下面的代码(建议直接放置到 1. 初始化 user,赋值 之前),指定cglib 源码生成目录,建议生成到 idea 项目中,可以直接打开生成的 class 字节码。// 设置 cglib 源码生成目录String sourcePath = “/Users/lcm/git/mica/mica-example/web-example/src/test/java”;System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, sourcePath);再次执行 main 方法。我们可以看到控制台打印下了这么一行日志。CGLIB debugging enabled, writing to ‘/Users/lcm/git/mica/mica-example/web-example/src/test/java’下面我们来看看生成的代码:看到此图大家恍然大悟,Cglib BeanCopier 帮我们生成了 get set 转换。Cglib copy 问题不支持链式 bean,mybatis-plus 生成的 Model 中默认添加了 @Accessors(chain = true) 注解默认为链式。不支持 原始类型和封装类型 copy int <-> Integer。类型转换不够智能,设置 useConverter 为 true 和重写 Converter,类型相同也会走转换的逻辑。注意:这部分后面会详细介绍,喜欢的朋友请关注、订阅我们。链接mica:https://github.com/lets-mica/mica如梦技术官网:https://www.dreamlu.net开源推荐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 26, 2019 · 1 min · jiezi

扩展spring cache 支持缓存多租户及其自动过期

spring cache 的概念Spring 支持基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。@Cacheable 使用效果 ,更具 cacheName(value) + 请求入参 (key) 组成保存redis中的keypublic class PigxClientDetailsService extends JdbcClientDetailsService { @Cacheable(value = SecurityConstants.CLIENT_DETAILS_KEY, key = “#clientId”) public ClientDetails loadClientByClientId(String clientId) { return super.loadClientByClientId(clientId); }}}多租户下缓存问题分析默认情况 A租户入参为K1 请求 应用,spring cache 会自动缓存 K1 的值,如果B租户 入参同时为K1 请求应用时,spring cache 还是会自动关联到同一个 Redis K1 上边查询数据。在多租户下 A/B 租户所请求的K1 并不是同一入参(虽然看起来参数名 参数值都是一样的),更不能返回同一个结果。默认的spring cache 根据入参来区分 不能满足多租户系统的设计需求,不能实现根据租户隔离。区分缓存增加租户标识A租户入参为K1 ,spring cache 维护Redis Key 在拼接一个租户信息KEY = cacheName + 入参 + 租户标识这样A/B 租户请求参数相同时,读取的也是不同的Key 里面的值,避免数据脏读,保证隔离型重写Spring Cache 的 cacheManager 缓存管理器从上下文中获取租户ID,重写@Cacheable value 值即可完成,然后注入这个 cacheManager@Slf4jpublic class RedisAutoCacheManager extends RedisCacheManager { /** * 从上下文中获取租户ID,重写@Cacheable value 值 * @param name * @return */ @Override public Cache getCache(String name) { return super.getCache(TenantContextHolder.getTenantId() + StrUtil.COLON + name); }}为什么要用 StrUtil.COLON 即 ‘:’ 分割在GUI 工具中,会通过’:‘的分隔符,进行分组,展示效果会更好增加 spring cache 的主动过期功能默认的注解里面没有关于时间的入参,如下图public @interface Cacheable { @AliasFor(“cacheNames”) String[] value() default {}; @AliasFor(“value”) String[] cacheNames() default {}; String key() default “”; String keyGenerator() default “”; String cacheManager() default “”; String cacheResolver() default “”; String condition() default “”; String unless() default “”; boolean sync() default false;}还是以value作为入口 value = “menu_details#2000” 通过对vaue 追加一个数字 并通过特殊字符分割,作为过期时间入参@Service@AllArgsConstructorpublic class PigXMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService { private final SysRoleMenuMapper sysRoleMenuMapper; @Override @Cacheable(value = “menu_details#2000”, key = “#roleId + ‘_menu’”) public List<MenuVO> findMenuByRoleId(Integer roleId) { return baseMapper.listMenusByRoleId(roleId); }}重写cachemanager 另个重要的方法 创建缓存的方法,通过截取 value 中设置的过期时间,赋值给你RedisCacheConfigurationpublic class RedisAutoCacheManager extends RedisCacheManager { private static final String SPLIT_FLAG = “#”; private static final int CACHE_LENGTH = 2; @Override protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) { if (StrUtil.isBlank(name) || !name.contains(SPLIT_FLAG)) { return super.createRedisCache(name, cacheConfig); } String[] cacheArray = name.split(SPLIT_FLAG); if (cacheArray.length < CACHE_LENGTH) { return super.createRedisCache(name, cacheConfig); } if (cacheConfig != null) { long cacheAge = Long.parseLong(cacheArray[1]); cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(cacheAge)); } return super.createRedisCache(name, cacheConfig); }}spring cache 操作缓存时 获取到上步设置的ttl 赋值给key @Override public void put(Object key, @Nullable Object value) { Object cacheValue = preProcessCacheValue(value); if (!isAllowNullValues() && cacheValue == null) { throw new IllegalArgumentException(String.format( “Cache ‘%s’ does not allow ’null’ values. Avoid storing null via ‘@Cacheable(unless="#result == null")’ or configure RedisCache to allow ’null’ via RedisCacheConfiguration.”, name)); } cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl()); }总结通过对spring cache 的扩展即可实现对缓存 一些透明操作cachemanager 是springcache 对外提供的API 扩展入口以上源码参考个人项目 基于Spring Cloud、OAuth2.0开发基于Vue前后分离的开发平台QQ: 2270033969 一起来聊聊你们是咋用 spring cloud 的吧。欢迎关注我们的公众号获得更多的好玩JavaEE 实践 ...

March 25, 2019 · 2 min · jiezi

联科首个开源项目启动!未来可期,诚邀加入!

OpenEA开源组织是广州市联科软件有限公司旗下的一个“开放·共享·全球化”的开源组织。OpenEA全称“Open+Enterprise+Application”,意为开放的企业应用,致力让所有企业都能轻松用上流程应用开发平台。2019年联科将逐步开源基于流程应用的快速开发平台,以“专业·高效·创新·自由·开放·回馈”为理念,以“开源之林”为目标,构建开放的技术生态圈。最近【流程设计器组件FlowDesigner】作为第一个开源项目,主要用于设计和控制流程各个运转过程,欢迎广大技术好友前来码云交流探讨!流程设计器组件FlowDesigner前言FlowDesigner来源于Linkey BPM中的流程设计器,作用于流程运行过程中的图形描述。它的操作简捷轻巧,能快速绘制出流程图。组件单独也可以使用,并能嵌入到任何需要该组件的系统中。分享,是“开源”的真谛。机不可失失不再来,准备好加入我们了吗?立即前往码云Fork项目吧,地址:https://gitee.com/openEA/Flow…

March 25, 2019 · 1 min · jiezi

Spring高阶用法--自定义业务对象组件化

若干年前在使用SpringMVC的时候,发现springMVC可以把HttpSession,HttpRequest组件化注入:@AutowiredHttpSession session;@AutowiredHttpRequest httpRequest;于是花了30分钟追踪了相关的源代码彻底摸清其原理,并且决定将用户(User/Principle)也组件化(尽管当时工作处于极其忙碌的状态,也忍不住去研究)。**方法如下:**1. 定义IPrincipal(IUser)接口 interface IPrincipal extends Serializable { IPrincipal get() } 2. 实现PrincipalObjectFactory class PrincipalObjectFactory implements ObjectFactory<IPrincipal>, Serializable { @Override IPrincipal getObject() { def requestAttr = RequestContextHolder.currentRequestAttributes() def request = requestAttr.getRequest() def p = new PrincipalHelper(request).get() new IPrincipal() { @Override IPrincipal get() { p } } }}3. 在spring的上下文中注册依赖处理器 beanFactory.registerResolvableDependency(IPrincipal, new PrincipalObjectFactory()) 只需要以上步骤,即可使用@Autowired在业务代码中注入IPrincipal(IUser),并且保证其线程安全。**原理:**阅读spring源码会发现,spring在注入接口时如果发现没有接口的实现类,就会从ResolvableDependency中寻找相关的依赖解决器。如果注册了相关的依赖解决器,会给此接口注入一个代理类,这个代理类的target就是ObjectFactory#getObject,在这里就可实现你的IPrincipal(IUser)获取了。总结1 使用这个方式将IPrincipal(IUser)组件化,而不是通过工具类的方式去获取。 **这样的方式充分体现了spring的依赖注入的思想,并且系统耦合性也降低不少。**2 即使在spring上下文中注入ObjectFactory,spring并不会自动注册,需要手动注册。

March 20, 2019 · 1 min · jiezi

扩展资源服务器解决oauth2 性能瓶颈

用户携带token 请求资源服务器资源服务器拦截器 携带token 去认证服务器 调用tokenstore 对token 合法性校验资源服务器拿到token,默认只会含有用户名信息通过用户名调用userdetailsservice.loadbyusername 查询用户全部信息详细性能瓶颈分析,请参考上篇文章《扩展jwt解决oauth2 性能瓶颈》 本文是针对传统使用UUID token 的情况进行扩展,提高系统的吞吐率,解决性能瓶颈的问题默认check-token 解析逻辑RemoteTokenServices 入口@Overridepublic OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>(); formData.add(tokenName, accessToken); HttpHeaders headers = new HttpHeaders(); headers.set(“Authorization”, getAuthorizationHeader(clientId, clientSecret)); // 调用认证服务器的check-token 接口检查token Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers); return tokenConverter.extractAuthentication(map);}解析认证服务器返回的信息DefaultAccessTokenConverterpublic OAuth2Authentication extractAuthentication(Map<String, ?> map) { Map<String, String> parameters = new HashMap<String, String>(); Set<String> scope = extractScope(map); // 主要是 用户的信息的抽取 Authentication user = userTokenConverter.extractAuthentication(map); // 一些oauth2 信息的填充 OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null, null); return new OAuth2Authentication(request, user); }组装当前用户信息DefaultUserAuthenticationConverterpublic Authentication extractAuthentication(Map<String, ?> map) { if (map.containsKey(USERNAME)) { Object principal = map.get(USERNAME); Collection<? extends GrantedAuthority> authorities = getAuthorities(map); if (userDetailsService != null) { UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME)); authorities = user.getAuthorities(); principal = user; } return new UsernamePasswordAuthenticationToken(principal, “N/A”, authorities); } return null;}问题分析认证服务器check-token 返回的全部信息资源服务器在根据返回信息组装用户信息的时候,只是用了username如果设置了 userDetailsService 的实现则去调用 loadUserByUsername 再去查询一次用户信息造成问题现象如果设置了userDetailsService 即可在spring security 上下文获取用户的全部信息,不设置则只能得到用户名。增加了一次查询逻辑,对性能产生不必要的影响解决问题扩展UserAuthenticationConverter 的解析过程,把认证服务器返回的信息全部组装到spring security的上下文对象中/** * @author lengleng * @date 2019-03-07 * <p> * 根据checktoken 的结果转化用户信息 */public class PigxUserAuthenticationConverter implements UserAuthenticationConverter { private static final String N_A = “N/A”; // map 是check-token 返回的全部信息 @Override public Authentication extractAuthentication(Map<String, ?> map) { if (map.containsKey(USERNAME)) { Collection<? extends GrantedAuthority> authorities = getAuthorities(map); String username = (String) map.get(USERNAME); Integer id = (Integer) map.get(SecurityConstants.DETAILS_USER_ID); Integer deptId = (Integer) map.get(SecurityConstants.DETAILS_DEPT_ID); Integer tenantId = (Integer) map.get(SecurityConstants.DETAILS_TENANT_ID); PigxUser user = new PigxUser(id, deptId, tenantId, username, N_A, true , true, true, true, authorities); return new UsernamePasswordAuthenticationToken(user, N_A, authorities); } return null; }}给remoteTokenServices 注入这个实现public class PigxResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter(); accessTokenConverter.setUserTokenConverter(userTokenConverter); remoteTokenServices.setRestTemplate(lbRestTemplate); remoteTokenServices.setAccessTokenConverter(accessTokenConverter); resources. .tokenServices(remoteTokenServices); }}完成扩展,再来看文章开头的流程图就变成了如下关注我个人项目 基于Spring Cloud、OAuth2.0开发基于Vue前后分离的开发平台QQ: 2270033969 一起来聊聊你们是咋用 spring cloud 的吧。 ...

March 20, 2019 · 2 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

Spring Boot创建定时任务

项目中经常要用到定时任务,比如发邮件短信、清理缓存等等spingboot 创建定时任务非常简单,只需要几个注解就可以。下面我给一个定时清理缓存的任务,测试程序缓存功能的时候经常要用到。1、启动定时任务配置只需要在 Application上加上 @EnableScheduling 注解, @EnableCaching是启动缓存配置的2、创建需要定时执行的方法在方法上加上注解 @Scheduled(fixedRate=10000) ,下图是一个定时清理缓存的方法每10s执行一次:执行结果:参加spring官方案例: https://spring.io/guides/gs/s...3、@Scheduled注解参数:@Scheduled(fixedRate = 5000) :上一次开始执行时间点之后5秒再执行@Scheduled(fixedDelay = 5000) :上一次执行完毕时间点之后5秒再执行这个与fixedRate区别在于,可以保证任务不会重叠执行,**fixedRate=5000表示每5s中启动任务,如果任务执行时间超过了5s中那么就会有多个任务同时执行。**fixedDelay=5000s会等带上个任务执行完毕才执行,@Scheduled(initialDelay=1000, fixedRate=5000) :第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次@Scheduled(cron="*/5 * * * * *") :通过cron表达式定义规则详细请看官方文档:

March 19, 2019 · 1 min · jiezi

现在最不缺的应该就是码农了,缺的是技术过硬又精通业务的工程师

昨天,一位分析界的老前辈对我很无奈地摇摇头,“这帮程序员,不食人间烟火哪!”我也深有感触,全世界的码农都一个鸟样。这让我想起了,同样也是他,在多年之前,对我提了警醒——要重视业务。从那之后,我一直狂奔在技术+业务的双修道路上。放在以前,码农这个族群一定是稀罕动物。但在今天,这个世界最不缺的应该就是码农了,未来最廉价的也将是码农。仅有泛泛一技,在未来并不吃香,因为那是要被机器人所取代的。这个世界,缺的是技术过硬又精通业务的工程师,缺的是真正能解决实际业务问题的人,缺的是复合型的人才。码农不是工程师,码农只是会写代码,只会明确需求和逻辑的情况下写代码。工程师则不一样,懂得用技术怎么解决实际业务问题,用技术驱动业务的发展。什么叫业务?先来明确这个问题。业务是一个很实在的东西,看得见感受得到,接地气儿。业务就是我们所能理解和感受的世界,就是这个世界或者某个行业的运转逻辑、流程与现状,是结果表象,是能够被看见和感受的,也是内在本质,是能够被洞察和感知的。业务就是这个世界发生了什么,什么时候,谁参与,怎么发生,结果如何。业务就是什么时候,谁在哪里,买了什么东西,花了多少钱,用什么支付。业务就是这个行业怎么发展起来的,现状如何,未来趋势如何,用了什么技术,有什么企业,商业模式如何,盈利能力如何,目前主要面临什么问题,消费者有什么特点,等等。世界很复杂,单个细分行业的业务也很复杂。为什么要了解业务?谈到这个,码农们一定有所不悦,“熟悉业务是需求分析师做的事,跟我们没有关系。”打个不恰当的比喻。有10个人经过一栋写字楼,突然从楼上掉下来几块砖头,砸中了9个人,其中就有7个码农,3个硕士,1个博士(原谅我又犯职业病,拿数据说话了)。而没被砸到的那个人,恰好因为了解到之前经常发生这样的事而绕道行走。如果你只会写代码,你不是不可替代的,而是可有可无的。因为这年头,会JAVA、C、Python的程序员,在大街上一抓一大把。现在已经开始提倡,编程从娃娃抓起了。10后都开始跟你抢饭碗了,你怕不怕?但话也不是那么极端,除非你的技术很牛逼,在国内或者某个行业内能够排上号的。但技术牛逼的人,也不是只是技术超群,还常常因为能够利用手中的技术解决某方面的业务问题,做了哪些突出的贡献。我们出来混,也是要拿成果说话的,做过什么项目,有什么价值。这种价值往往就是针对业务而说的。IT研发与业务需求方常常是一对冤家,常常因为一个业务功能实现争辩得耳红面赤。研发觉得这个功能很low,没什么技术含量,业务方却认为这个功能却很有用,需要花功夫做细做深做好。现实情况是,功能做出来了,却很难用,或者经常用不了,或者数据不对。研发想做点高大上的功能,业务方却认为太虚了,没什么用。(IT与业务方那点事就不多说了,大家都心知肚明~~)多年经验反复告诫我,鉴定一个功能是不是好功能,唯一的标准是看它能否支撑业务、改善业务、推动业务,也即应用效果。一个产品,只要有30%的功能,让业务用户用起来很爽,感觉帮助很大,就已经是一个不错的产品了。我们都认同,技术驱动业务。但我们不一定明白,正是由于业务的某些强烈需求,才推动技术的发展与落地。说这些,我是想说,作为技术人员,我们既要仰望星空,也要脚踏实地,既要追逐腾飞的技术,也要重视落地的业务。如果一个业务人员很懂技术,那将很可能是技术人员的灾难。因为那样的话,业务人员会很强势,又或者那样就没有技术人员什么事了。当然,也不难想象,一个真正懂看数据的测试人员,就好比一个真正懂用算法的业务人员一样难得。业务与数据的关系真实(而不是杜撰、模拟、伪造)、可量化、可被记录的数据一定会反映真实世界某方面的业务情形。而现实当中很多业务场景都可由数据体现出来。零售是业务场景最繁多且最贴近我们生活的行业,可以从中找到很多方便理解的例子。当你在一个酷热难耐的夏天上午10点,走进位于公司附近的全家便利店,使用微信支付,花了3.5元,买了一瓶无糖330ml摩登罐的可乐,而且刷会员卡攒了100积分,而收银员MM返回给了你一张POS单据,这时你所发生的这一切都已经通过收银记录在了全家的数据库里。更糟糕的是,店里的摄像头也已经把你在店里的一举一动录了下来了,转化成为一帧帧图像数据。这就是,业务数据化。店长通过数据分析发现,最近3.5元330ml摩登罐可乐的销量比上月增长了20%,而消费者中75%是20-35岁的男性,相比之下,300ml塑料瓶装的可乐销量却下滑40%。店长权衡比较了一下,300ml塑料瓶装可乐利润低,330ml摩登罐可乐目前更受年轻人欢迎,考虑到日渐增长的租金压力,做了一个大胆的决定——下架300ml塑料瓶装可乐,增加330ml摩登罐可乐的商品。(又拿数据说话了。)这就是,数据业务化。或者,数据驱动业务。当我开始接触一个行业时,我通常会花2-3周的时间去了解这个行业的业务,然后就大致清楚这个行业有什么样的数据,可以做哪方面的分析,解决什么问题。当遇到不好理解的分析结果时,我经常使用业务联想法,设身处地去体会结果所反映的业务场景是什么样的。如何了解业务?这个说大了,就是如何看这个世界。每个人有每个人的方法论,每个人有每个人的世界观,每个人有每个人的逻辑思维。我们都知道,观念的转变是最难的,也有很多不确定性。有些人可能因为自己的切身体会一天就改变了之前几十年根深蒂固的看法,有些人任由三姑六婆苦口婆心地劝说就是不肯改变自己的择偶观,却有可能因为自己年岁渐大不断降低自己的标准。但最好也及早要形成科学的思考方法,帮助正确地理解这个世界。以“面-线-点”的方式可以较为全面、系统、深入地了解一个行业,然后是某个垂直领域,最后再到具体业务场景。佛系文化的流行,使得年轻一代降低对这个世界的关注度,一切都无所谓,一切都漠不关心。这个世界从来没有变好过,但我们每个人都是这个世界的匆匆过客,都是行走在自己的人生路上不断领略这个世界的美与丑。这世间的风景,这世间的悲欢离合,如果我们积极地探索与领悟,也不枉来这世间走一遭。保持好奇心,可以驱动我们的思考,强化我们的认知,丰富我们的内在。这是我想说的第二个方面。怀有好奇心,就会渐渐地敏锐观察这个世界,多问自己一些为什么。我家附近原来有个沃尔玛超市,现在地产商将它装修一番,引入了不少餐厅,刚开张不久,我就去那里吃饭,吃的是烤鸭,一个多两个月后,再去那里吃饭,发现有一半的餐厅已经关门了。在去地铁站的那条路上,每天人流如梭,一点点,即使到了深夜,依然有很多人在门口排队买奶茶。然而,仅仅隔了一个店铺的喜茶,做不下去,关门了。两三个月前又换成粉店,路转粉。每天下班路过时,发现店里顾客不到10个,门可罗雀。为什么每家一点点奶茶店门口,不管是什么时候都是很多人,他们是托儿还是真的顾客?因为喜欢新鲜,不喜欢在冰箱里存太多菜,且附近没有菜市场,所以常去买菜的还是附近的钱大妈。但我却没怎么去更近的一家生活超市,店面比较大,除果肉蔬菜外,也卖油盐酱醋,还有生活用品,但奇怪的是顾客却不到钱大妈的1/10。为什么几乎所有潮州牛肉店都很多人,有很多甚至在门口排了很长的队?观察到这些,常常会陷入思考,为什么会发生这些,新零售到底改变了什么?再举个例子。去年拿保温杯泡着枸杞的中年男火了。关于这个,我又问了自己几个问题:拿着保温杯泡着枸杞的是不是都是中年男?如果是,这个特征能否被数据量化?可否考虑加入到算法模型当中,加以应用起来?虽然很多问题,我没有找到答案,但多问自己问题,会引发自己不断深入思考,不断激发自己好奇心,不断去研究。很多业务知识都是零散的,不可能在短时间内完全了解,可以在日常不断积累。关于日常积累业务知识,可以经常询问懂业务的人。这是我想说的第三个方面。刚进公司的时候,我以为业务很简单。很快,我就发现里面的坑不少。加上所在团队的成员也是刚入职不久的,问问题没处可问。过了一个月之后,我发现隔壁团队有两个十年左右的老员工,业务很熟,而且人特好。于是,我几乎一遇到业务问题,就跑过去“骚扰”他们,他们也很乐意解答,如果他们不清楚,他们也会告诉我应该去找谁了解。大约半年之后,我基本摸透了顺丰的数据和业务情况。我也和那两位老员工建立了不错的友谊,即使后来换了部门,我也经常过去找他们。跟懂业务的人搞好关系,遇到业务问题,多咨询他们,这是最有效最接地气的办法。多看书,这是我想说的第四个方面。比如说,从事新零售领域方面的工作,总得先了解新零售是怎么回事。你可以去听专家们忽悠,但这样的机会很少,而且时间也有限,说不定成本还很高。读书则不一样。读书,意味着主动了解,主动去构建自己的知识体系。读书的重要性,这里不多言了。如果您读这篇文章的时候,您恰好也是一位数据人。我还想告诫一句:我们不能脱离业务去看数据,而是要时刻从业务角度去理解数据。我们不敢期望可以完全理解这个世界,但也憧憬着我们不单可以在代码的世界里畅快驰骋,论剑江湖,也可以放下身段洞察芸芸众生之百态,领悟人间世俗之真情。如果真的可以的话,就没有需求分析师什么事了。硬实力这里说的硬实力,也就是技术上的真实积累。怎么来体现你的技术实力?我总的分为:技术深度和技术广度这两方面。技术广度通俗的讲,就是你熟悉该技术点的使用以及基本原理。一般面试官在面试首轮会问很多技术点,来考核你是否能正确使用。准备不充分的面试,完全是浪费时间,更是对自己的不负责(如果title很高,当我没说)。今天给大家分享下在跳槽时需要准备的Java面试大纲,其中大部分都是面试过程中的面试题,可以对照这查漏补缺,当然了,这里所列的肯定不可能覆盖全部方式。软实力软实力在面试过程中也尤为重要(有时候真的要更重要),主要是指和面试官的沟通,对一个问题的阐述方式和表达方式,逻辑思维能力等。面试过程全程微笑,项目描述需要严谨的表述,个人的优缺点基本要做到随口而出..等这些其实就是软实力的体现。知己知彼、百战不殆,面试也是如此,针对于上面的面试问到的知识点我总结出了互联网公司java程序员在面试中涉及到的绝大部分架构面试题及答案做成了文档和架构视频资料免费分享给大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术资料),希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!资料领取方式:关注+转发后,私信关键词 【架构资料】即可获取!重要的事情说三遍,转发、转发、转发后再发私信,才可以拿到哦!

March 18, 2019 · 1 min · jiezi

最简单的SpringBoot整合MyBatis教程

前面两篇文章和读者聊了Spring Boot中最简单的数据持久化方案JdbcTemplate,JdbcTemplate虽然简单,但是用的并不多,因为它没有MyBatis方便,在Spring+SpringMVC中整合MyBatis步骤还是有点复杂的,要配置多个Bean,Spring Boot中对此做了进一步的简化,使MyBatis基本上可以做到开箱即用,本文就来看看在Spring Boot中MyBatis要如何使用。工程创建首先创建一个基本的Spring Boot工程,添加Web依赖,MyBatis依赖以及MySQL驱动依赖,如下: 创建成功后,添加Druid依赖,并且锁定MySQL驱动版本,完整的依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.28</version> <scope>runtime</scope></dependency>如此,工程就算是创建成功了。读者注意,MyBatis和Druid依赖的命名和其他库的命名不太一样,是属于xxx-spring-boot-stater模式的,这表示该starter是由第三方提供的。基本用法MyBatis的使用和JdbcTemplate基本一致,首先也是在application.properties中配置数据库的基本信息:spring.datasource.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=utf-8spring.datasource.username=rootspring.datasource.password=rootspring.datasource.type=com.alibaba.druid.pool.DruidDataSource配置完成后,MyBatis就可以创建Mapper来使用了,例如我这里直接创建一个UserMapper2,如下:public interface UserMapper2 { @Select(“select * from user”) List<User> getAllUsers(); @Results({ @Result(property = “id”, column = “id”), @Result(property = “username”, column = “u”), @Result(property = “address”, column = “a”) }) @Select(“select username as u,address as a,id as id from user where id=#{id}”) User getUserById(Long id); @Select(“select * from user where username like concat(’%’,#{name},’%’)”) List<User> getUsersByName(String name); @Insert({“insert into user(username,address) values(#{username},#{address})”}) @SelectKey(statement = “select last_insert_id()”, keyProperty = “id”, before = false, resultType = Integer.class) Integer addUser(User user); @Update(“update user set username=#{username},address=#{address} where id=#{id}”) Integer updateUserById(User user); @Delete(“delete from user where id=#{id}”) Integer deleteUserById(Integer id);}这里是通过全注解的方式来写SQL,不写XML文件,@Select、@Insert、@Update以及@Delete四个注解分别对应XML中的select、insert、update以及delete标签,@Results注解类似于XML中的ResultMap映射文件(getUserById方法给查询结果的字段取别名主要是向小伙伴们演示下@Results注解的用法),另外使用@SelectKey注解可以实现主键回填的功能,即当数据插入成功后,插入成功的数据id会赋值到user对象的id属性上。 UserMapper2创建好之后,还要配置mapper扫描,有两种方式,一种是直接在UserMapper2上面添加@Mapper注解,这种方式有一个弊端就是所有的Mapper都要手动添加,要是落下一个就会报错,还有一个一劳永逸的办法就是直接在启动类上添加Mapper扫描,如下:@SpringBootApplication@MapperScan(basePackages = “org.sang.mybatis.mapper”)public class MybatisApplication { public static void main(String[] args) { SpringApplication.run(MybatisApplication.class, args); }}好了,做完这些工作就可以去测试Mapper的使用了。mapper映射当然,开发者也可以在XML中写SQL,例如创建一个UserMapper,如下:public interface UserMapper { List<User> getAllUser(); Integer addUser(User user); Integer updateUserById(User user); Integer deleteUserById(Integer id);}然后创建UserMapper.xml文件,如下:<?xml version=“1.0” encoding=“UTF-8” ?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=“org.sang.mybatis.mapper.UserMapper”> <select id=“getAllUser” resultType=“org.sang.mybatis.model.User”> select * from t_user; </select> <insert id=“addUser” parameterType=“org.sang.mybatis.model.User”> insert into user (username,address) values (#{username},#{address}); </insert> <update id=“updateUserById” parameterType=“org.sang.mybatis.model.User”> update user set username=#{username},address=#{address} where id=#{id} </update> <delete id=“deleteUserById”> delete from user where id=#{id} </delete></mapper>将接口中方法对应的SQL直接写在XML文件中。 那么这个UserMapper.xml到底放在哪里呢?有两个位置可以放,第一个是直接放在UserMapper所在的包下面: 放在这里的UserMapper.xml会被自动扫描到,但是有另外一个Maven带来的问题,就是java目录下的xml资源在项目打包时会被忽略掉,所以,如果UserMapper.xml放在包下,需要在pom.xml文件中再添加如下配置,避免打包时java目录下的XML文件被自动忽略掉:<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources></build>当然,UserMapper.xml也可以直接放在resources目录下,这样就不用担心打包时被忽略了,但是放在resources目录下,又不能自动被扫描到,需要添加额外配置。例如我在resources目录下创建mapper目录用来放mapper文件,如下: 此时在application.properties中告诉mybatis去哪里扫描mapper:mybatis.mapper-locations=classpath:mapper/.xml如此配置之后,mapper就可以正常使用了。注意第二种方式不需要在pom.xml文件中配置文件过滤。原理分析在SSM整合中,开发者需要自己提供两个Bean,一个SqlSessionFactoryBean,还有一个是MapperScannerConfigurer,在Spring Boot中,这两个东西虽然不用开发者自己提供了,但是并不意味着这两个Bean不需要了,在org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration类中,我们可以看到Spring Boot提供了这两个Bean,部分源码如下:@org.springframework.context.annotation.Configuration@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })@ConditionalOnSingleCandidate(DataSource.class)@EnableConfigurationProperties(MybatisProperties.class)@AutoConfigureAfter(DataSourceAutoConfiguration.class)public class MybatisAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); return factory.getObject(); } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } } @org.springframework.context.annotation.Configuration @Import({ AutoConfiguredMapperScannerRegistrar.class }) @ConditionalOnMissingBean(MapperFactoryBean.class) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { @Override public void afterPropertiesSet() { logger.debug(“No {} found.”, MapperFactoryBean.class.getName()); } }}从类上的注解可以看出,当当前类路径下存在SqlSessionFactory、 SqlSessionFactoryBean以及DataSource时,这里的配置才会生效,SqlSessionFactory和SqlTemplate都被提供了。为什么要看这段代码呢?下篇文章,松哥和大伙分享Spring Boot中MyBatis多数据源的配置时,这里将是一个重要的参考。 好了,本文就先说到这里,关于在Spring Boot中整合MyBatis,这里还有一个小小的视频教程,加入我的星球即可免费观看: 关于我的星球【Java达摩院】,大伙可以参考这篇文章推荐一个技术圈子,Java技能提升就靠它了. ...

March 18, 2019 · 2 min · jiezi

Spring Bean 是什么?

1、简介Bean 是 Spring Framework 的核心概念。因此,理解这一概念对于掌握框架并行之有效地使用它显得至关重要。不幸的是,关于这么简单地问题却没有正确的答案——Spring Bean 到底是什么。有些解释十分低级会使我们囿限于细节之中,而有些却非常模棱两可。这篇文章将尝试从官方文档中的描述开始阐明 Spring Bean 到底是什么。2、Bean 的定义这是官方文档中关于 Bean 的定义:In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.这个定义简明扼要,但是遗漏了一个重要的东西——Spring IoC Container。3、控制反转简而言之,Inversion of Control 或简称 IoC 是一个对象不用 new 创建它就能定义其依赖关系的过程。对象将创建依赖关系的任务交给了 IoC 容器。3.1、Domain 类假设我们声明一个类:public class Company { private Address address; public Company(Address address) { this.address = address; } // getter, setter and other properties}Company 类还需要一个 Address 类来协作它:public class Address { private String street; private int number; public Address(String street, int number) { this.street = street; this.number = number; } // getters and setters}3.2、传统做法一般情况下,我们使用类的构造函数来创建对象。Address address = new Address(“High Street”, 1000);Company company = new Company(address);这种方法本身没有任何问题,但是用更好的方法来管理依赖项不是更好嘛~想象一下一个有几十个甚至上百个类的应用程序,有时候我们希望在整个应用程序中共享一个类单个实例,甚至其他。管理如此多对象绝对是一个噩梦,这就是 IoC 来拯救我们的时候。对象可以从 IoC 容器中检索其依赖关系,而不是自己构造依赖关系。而我们需要做的就是为容器提供适当的配置元数据。3.3、Bean 的配置首先,让我们用 @Component 注解来修饰 Company 类:@Componentpublic class Company { // this body is the same as before}还有一个为 IoC 容器提供 Bean 元数据的配置类:@Configuration@ComponentScan(basePackageClasses = Company.class)public class Config { @Bean public Address getAddress() { return new Address(“High Street”, 1000); }}这个配置类生成一个 Address 类型的 bean。它还有 @ComponentScan 注解,这个注解指示容器在包含 Company 类的包中查找 bean。当 Spring IoC 容器构造这些类型的对象时,所有的对象都被成为 Spring bean,因为它们由 IoC 容器管理。3.4、IoC 实战因为我们在配置类中定义了 bean,所以我们需要一个 AnnotationConfigApplicationContext 类的实例来构建容器:ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);用一个快速测试来验证我们的 bean 的存在性和属性:Company company = context.getBean(“company”, Company.class);assertEquals(“High Street”, company.getAddress().getStreet());assertEquals(1000, company.getAddress().getNumber());出现的结果证明了 IoC 容器已经被创建并且 Bean 已经被正确初始化了。4、总结这篇文章简要介绍了 Spring Bean 以及其与 IoC 容器的关系。完整源码可以在 Github 上查看。 ...

March 18, 2019 · 1 min · jiezi

springboot 解决跨域

一、什么是跨域HTTP请求现代浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。跨域HTTP请求是指A域上资源请求了B域上的资源,举例而言,部署在A机器上Nginx上的js代码通过ajax请求了部署在B机器Tomcat上的RESTful接口。IP(域名)不同、或者端口不同,都会造成跨域问题。为了解决跨域的问题,曾经出现过jsonp、代理文件等方案,应用场景受限,维护成本高,直到HTML5带来了CORS协议。CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。它通过服务器增加一个特殊的Header[Access-Control-Allow-Origin]来告诉客户端跨域的限制,如果浏览器支持CORS、并且判断Origin通过的话,就会允许XMLHttpRequest发起跨域请求。CROS常见headerAccess-Control-Allow-Origin:http://somehost.com 表示允许http://somehost.com发起跨域请求。Access-Control-Max-Age:86400 表示在86400秒内不需要再发送预校验请求。Access-Control-Allow-Methods: GET,POST,PUT,DELETE 表示允许跨域请求的方法。Access-Control-Allow-Headers: content-type 表示允许跨域请求包含content-type二、CORS实现跨域访问授权方式方式1:返回新的CorsFilter方式2:重写WebMvcConfigurer方式3:使用注解(@CrossOrigin)方式4:手工设置响应头(HttpServletResponse )注:方式1和方式2属于全局CORS配置,方式3和方式4属于局部CORS配置。如果使用了局部跨域是会覆盖全局跨域的规则,所以可以通过@CrossOrigin注解来进行细粒度更高的跨域资源控制。1.返回新的CorsFilter(全局跨域)package com.hehe.yyweb.config;@Configurationpublic class GlobalCorsConfig { @Bean public CorsFilter corsFilter() { //1.添加CORS配置信息 CorsConfiguration config = new CorsConfiguration(); //放行哪些原始域 config.addAllowedOrigin(""); //是否发送Cookie信息 config.setAllowCredentials(true); //放行哪些原始域(请求方式) config.addAllowedMethod(""); //放行哪些原始域(头部信息) config.addAllowedHeader(""); //暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息) config.addExposedHeader(""); //2.添加映射路径 UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/", config); //3.返回新的CorsFilter. return new CorsFilter(configSource); }}2. 重写WebMvcConfigurer(全局跨域)任意配置类,返回一个新的WebMvcConfigurer Bean,并重写其提供的跨域请求处理的接口,目的是添加映射路径和具体的CORS配置信息。package com.hehe.yyweb.config;@Configurationpublic class GlobalCorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override //重写父类提供的跨域请求处理的接口 public void addCorsMappings(CorsRegistry registry) { //添加映射路径 registry.addMapping("/") //放行哪些原始域 .allowedOrigins("") //是否发送Cookie信息 .allowCredentials(true) //放行哪些原始域(请求方式) .allowedMethods(“GET”,“POST”, “PUT”, “DELETE”) //放行哪些原始域(头部信息) .allowedHeaders("") //暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息) .exposedHeaders(“Header1”, “Header2”); } }; }}3. 使用注解(局部跨域)在方法上(@RequestMapping)使用注解 @CrossOrigin :@RequestMapping("/hello")@ResponseBody@CrossOrigin(“http://localhost:8080”) public String index( ){ return “Hello World”;}或者在控制器(@Controller)上使用注解 @CrossOrigin :@Controller@CrossOrigin(origins = “http://xx-domain.com”, maxAge = 3600)public class AccountController { @RequestMapping("/hello") @ResponseBody public String index( ){ return “Hello World”; }}手工设置响应头(局部跨域 )使用HttpServletResponse对象添加响应头(Access-Control-Allow-Origin)来授权原始域,这里Origin的值也可以设置为"*" ,表示全部放行。@RequestMapping("/hello")@ResponseBodypublic String index(HttpServletResponse response){ response.addHeader(“Access-Control-Allow-Origin”, “http://localhost:8080”); return “Hello World”;}三、测试跨域访问首先使用 Spring Initializr 快速构建一个Maven工程,什么都不用改,在static目录下,添加一个页面:index.html 来模拟跨域访问。目标地址: http://localhost:8090/hello<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”/> <title>Page Index</title></head><body><h2>前台系统</h2><p id=“info”></p></body><script src=“webjars/jquery/3.2.1/jquery.js”></script><script> $.ajax({ url: ‘http://localhost:8090/hello’, type: “POST”, xhrFields: { withCredentials: true //允许跨域认证 }, success: function (data) { $("#info").html(“跨域访问成功:"+data); }, error: function (data) { $("#info”).html(“跨域失败!!”); } })</script></html>然后创建另一个工程,在Root Package添加Config目录并创建配置类来开启全局CORS。package com.hehe.yyweb.config;@Configurationpublic class GlobalCorsConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**"); } }; }}接着,简单编写一个Rest接口 ,并指定应用端口为8090。package com.hehe.yyweb;@SpringBootApplication@RestControllerpublic class YyWebApplication { @Bean public TomcatServletWebServerFactory tomcat() { TomcatServletWebServerFactory tomcatFactory = new TomcatServletWebServerFactory(); tomcatFactory.setPort(8090); //默认启动8090端口 return tomcatFactory; } @RequestMapping("/hello") public String index() { return “Hello World”; } public static void main(String[] args) { SpringApplication.run(YyWebApplication.class, args); }}最后分别启动两个应用,然后在浏览器访问:http://localhost:8080/index.html ,可以正常接收JSON数据,说明跨域访问成功!!原文链接:http://www.jianshu.com/p/477e… ...

March 18, 2019 · 2 min · jiezi

扩展jwt解决oauth2 性能瓶颈

oauth2 性能瓶颈资源服务器的请求都会被拦截 到认证服务器校验合法性 (如下图)用户携带token 请求资源服务器资源服务器拦截器 携带token 去认证服务器 调用tokenstore 对token 合法性校验资源服务器拿到token,默认只会含有用户名信息通过用户名调用userdetailsservice.loadbyusername 查询用户全部信息如上步骤在实际使用,会造成认证中心的负载压力过大,成为造成整个系统瓶颈的关键点。check-token 过程中涉及的源码更为详细的源码讲解可以参考我上篇文章《Spring Cloud OAuth2 资源服务器CheckToken 源码解析》check-token 涉及到的核心类扩展jwt 生成携带用户详细信息为什么使用jwt 替代默认的UUID token ?通过jwt 访问资源服务器后,不再使用check-token 过程,通过对jwt 的解析即可实现身份验证,登录信息的传递。减少网络开销,提高整体微服务集群的性能spring security oauth 默认的jwttoken 只含有username,通过扩展TokenEnhancer,实现关键字段的注入到 JWT 中,方便资源服务器使用 @Bean public TokenEnhancer tokenEnhancer() { return (accessToken, authentication) -> { if (SecurityConstants.CLIENT_CREDENTIALS .equals(authentication.getOAuth2Request().getGrantType())) { return accessToken; } final Map<String, Object> additionalInfo = new HashMap<>(8); PigxUser pigxUser = (PigxUser) authentication.getUserAuthentication().getPrincipal(); additionalInfo.put(“user_id”, pigxUser.getId()); additionalInfo.put(“username”, pigxUser.getUsername()); additionalInfo.put(“dept_id”, pigxUser.getDeptId()); additionalInfo.put(“tenant_id”, pigxUser.getTenantId()); additionalInfo.put(“license”, SecurityConstants.PIGX_LICENSE); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; }; }生成的token 如下,含有关键的字段重写默认的资源服务器处理行为不再使用RemoteTokenServices 去掉用认证中心 CheckToken,自定义客户端TokenService@Slf4jpublic class PigxCustomTokenServices implements ResourceServerTokenServices { @Setter private TokenStore tokenStore; @Setter private DefaultAccessTokenConverter defaultAccessTokenConverter; @Setter private JwtAccessTokenConverter jwtAccessTokenConverter; @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken); UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter(); defaultAccessTokenConverter.setUserTokenConverter(userTokenConverter); Map<String, ?> map = jwtAccessTokenConverter.convertAccessToken(readAccessToken(accessToken), oAuth2Authentication); return defaultAccessTokenConverter.extractAuthentication(map); } @Override public OAuth2AccessToken readAccessToken(String accessToken) { return tokenStore.readAccessToken(accessToken); }}解析jwt 组装成Authentication/** * @author lengleng * @date 2019-03-17 * <p> * jwt 转化用户信息 */public class PigxUserAuthenticationConverter implements UserAuthenticationConverter { private static final String USER_ID = “user_id”; private static final String DEPT_ID = “dept_id”; private static final String TENANT_ID = “tenant_id”; private static final String N_A = “N/A”; @Override public Authentication extractAuthentication(Map<String, ?> map) { if (map.containsKey(USERNAME)) { Collection<? extends GrantedAuthority> authorities = getAuthorities(map); String username = (String) map.get(USERNAME); Integer id = (Integer) map.get(USER_ID); Integer deptId = (Integer) map.get(DEPT_ID); Integer tenantId = (Integer) map.get(TENANT_ID); PigxUser user = new PigxUser(id, deptId, tenantId, username, N_A, true , true, true, true, authorities); return new UsernamePasswordAuthenticationToken(user, N_A, authorities); } return null; } private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) { Object authorities = map.get(AUTHORITIES); if (authorities instanceof String) { return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities); } if (authorities instanceof Collection) { return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils .collectionToCommaDelimitedString((Collection<?>) authorities)); } throw new IllegalArgumentException(“Authorities must be either a String or a Collection”); }}资源服务器配置中注入以上配置即可@Slf4jpublic class PigxResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter(); accessTokenConverter.setUserTokenConverter(userTokenConverter); PigxCustomTokenServices tokenServices = new PigxCustomTokenServices(); // 这里的签名key 保持和认证中心一致 JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(“123”); converter.setVerifier(new MacSigner(“123”)); JwtTokenStore jwtTokenStore = new JwtTokenStore(converter); tokenServices.setTokenStore(jwtTokenStore); tokenServices.setJwtAccessTokenConverter(converter); tokenServices.setDefaultAccessTokenConverter(accessTokenConverter); resources .authenticationEntryPoint(resourceAuthExceptionEntryPoint) .tokenServices(tokenServices); }}使用JWT 扩展后带来的问题JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。去认证服务器校验的过程就是 通过tokenstore 来控制jwt 安全性的一个方法,去掉Check-token 意味着 jwt token 安全性不可保证JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。关注我个人项目 基于Spring Cloud、OAuth2.0开发基于Vue前后分离的开发平台QQ: 2270033969 一起来聊聊你们是咋用 spring cloud 的吧。 ...

March 18, 2019 · 2 min · jiezi

Java到底要做到什么程度才能适应市场的需求(本人的面试经历)

前言:从过年前就萌生出要跳槽的想法,到过年来公司从月初提出离职到~~号正式离职,上班的时间也出去面试过几家公司,后来总觉的在职找工作总是得请假,便决心离职后找工作。到3月10号找到了一家互联网公司成功应聘上,中间也经历了很多公司,有外包的、创业的、互联网的等等各种类型,也收到了很多offer,也有面试不顺序的…今天来记录一下自己面试中的问题,围绕着java到底应该具备什么样的水平才能适应现在市场的要求的主题来谈一谈。本篇文章目录:一:面试中的问题二: 面试中要注意的问题三:关于最后的选择四:两年java到底应该具备什么样的水平一:面试中的问题java集合框架:1:介绍一下java的集合框架2:HashMap遇见哈希冲突会如何怎么办?HashMap是线程安全的吗?HashMap在高并发下会有什么问题?然后引入ConcurrentHashMap的原理?3:Hahtable和concurrentHashMap的区别?4:数组和ArrayList的区别?Arraylist是如何扩容的?5:线程池中的阻塞队列一般会选择哪种队列?为什么?6:RetreenLock的原理?AQS的原理?7:HashMap的容量为什么推荐是2的幂次方?框架类:1:mybatis的二级缓存有什么问题?2:mybaits中的mapper的#{}和${}有什么区别?哪种可以防止sql注入?2:我们知道mybatis的mapper和接口之间是没有对象的,那么它是如何映射的?4:说说springmvc的注解有哪些?他们的原理是什么?5:springmvc的控制器是单例的吗?是线程安全的吗?6:struts1和struts2的区别?是线程安全的吗?7:spring如何解析它的xml文件?8:spring的核心是什么?Aop的原理是什么?redis相关:1:redis数据类型有哪些?2:zset数据类型是如何排序的?3:redis如何做项目的中间缓存层?4:redis的Hash的时间复杂度是多少?数据库:1:数据库索引分为哪几种?组合索引有什么要注意的问题?2:什么是悲观锁 什么是乐观锁?如何实现悲观锁?3: 数据库关键字的执行顺序是什么?4:如何进行sql优化?5:有没有进行过分库分表操作?分库之后如何保持事务一致?分布式和微服务:1:微服务要克服那些问题?微服务系统是怎样通信的?2:分布式环境下如何解决session不一致的问题?3:分布式下如何保证id一致?4:你在dubbo的使用过程中遇到什么问题?5: zookeeper的负载均衡算法有哪些?jdk源码相关1:synchronized的原理?它该怎么用?如何一个方法是synchronized的,其他的非synchronzied线程能进入吗?2:cvs中的ABA问题如何解决?3:volatile的原理是什么?volatile一定是线程安全的吗?4:ThreadLocal是什么?它的原理是什么?5:CountDowanLatch有没有用过?适合在什么样的场景下用?设计模式相关:1:实现两种单例模式2:讲一下观察者模式3:spring中都用到哪些设计模式?4:动态代理模式是如何实现的?5:你在项目中用到哪些设计模式了?讲解一下业务场景算法相关:1:快速排序的时间复杂度?手写快速排序(注意递归式和非递归式的实现方式)2:手写二分查找3:手写堆排序4:一个int数组如何进行奇数和偶数分离?5:用算法实现String转doublejvm相关:1: jvm的垃圾回收算法有哪些?分别解释一下?2: 新生代为什么要设置两个survior区?3:如何通过一个.class文件获取它的jdk版本?4:jvm的内存模型?哪些是线程私有的?哪些是公共的?关于自己的项目(问的时间最长)1:简述一下自己的项目?你在其中主要是做什么的?2:你在项目中都遇到了哪些难题?最后都是怎么解决的?3:项目有多大规模?周期多久(这个很多都问到的)4:讲一下某一模块的具体实现方式?然后从中挑刺5:如何解决某一时刻的高并发请求?6:如何解决订单支付回调的超时问题?轮询应该怎么写?其他:1:秒杀场景如何削峰?2:http和udp的区别是什么?3:ajax的跨域问题4:nio与io的区别?什么情况下适合用nio5: 说说常见的linux命令,linux查看内存的命令是什么?7:git遇见代码冲突了怎么办?8:说几个常见的maven命令,maven如何排除一个jar包的冲突?二: 面试中要注意的问题2.1:一定要有自己的实际项目经验按照我这么多面试经验?其实有的公司会侧重于问自己做的项目经验,有的公司侧重于问问题,一般互联网公司会对技术要求比较高,既要求项目经验又要要求技术水平2.2:可以适当渲染,但是不要夸大其词面试的过程中最忌讳的就是夸夸其谈,高屋建瓴很厉害,但是一到实际细节都不知所云了,在技术总监面前,其实你吹牛或者是真的会他是一目了然的。不懂装懂,有的面试官又给你台阶下,不然你就卡带了,这很容易造成面试的不好印象2.3:要会自我介绍面试的时候一般的话都会让你做一个自我介绍,这个要分对象,是技术官还是Hr,如果是技术官侧重于综述一下自己的项目的实际技术栈和技术路线,如果是Hr的话不要用过多的技术语言,而要说一些自己的实际工作经历或者自己上家公司的运营情况2.4:关于简历简历切记不可太啰嗦,但是不可太简单,作为技术的简历一般起码得在3页,不然HR会觉得你的求职态度不怎么好,不管如何求职结果如何,一个良好的简历会给人留下好的第一印象(有简历模板)三:关于最后的选择说实话也接受到很多HR的offer邀请,但是我一般会选择说考虑一下一天以后再给回复,切不可直接把话说死,不然后面就尴尬了。实际提供的offer的有一家外包公司,三家创业公司,两家互联网公司,最终选择了一家互联网公司,虽然实际上班地点有点远(下了地铁还得座公交,后来还是选择骑单车了),但是互联网公司会给你快的成长速度,并且互联网技术栈都比较新..相比于传统企业会有更多的技术挑战。而外包公司的话,可能环境不怎么好,我记得自己当初还是个小白的时候,去了外包,那里的优点就是会有不断的活,新人进去的话收获还是挺多的,但是作为已经有两年经验的我,外包很显然不适合我的后期职业发展。缺点:技术更新迭代的太慢,没有归属感,最后的选择我个人的意见是选择技术优先,毕竟以后软件路还长,技术才是王道四:两年java到底应该具备什么样的水平两年java的面试过程中遇到了很多挑战,也遇到了一些不谈技术的公司,从上面的面试题可以看出,目前对于java的要求越来越高,水涨船高,毕竟这个行业的人数越来越多,而保持自己的竞争力的唯一方法就是找对方向,不断学习,注意这里我提到的第一点是方向,然后才是学习。给自己制定一个职业规划,按照这个路线往前走,我其实还在想分布式微服务这块以后再深入学习,可是按照市场要求,现在已经刻不容缓了,一些技术架构比如:springcloud、duboo都得保持学习,这样才能有竞争力!作为一名两年的javaSir,你必须具备以下技能1:阅读源码的能力,多用Intelj idea这个开发工具,而不是eclipse。它是直接支持反编译class文件的,多读jdk源码,吸收优秀的源码并加以复用2:做到能够手写常见的排序算法,比如快速排序和堆排序、冒泡排序、选择排序、二分查找这些都是必须的3:对java的框架有很深入的认识,现在基本流行的ssm框架很多人都会,可是知道一些原理的人就不多了,得不断研究这些框架本身,它们都是经过无数次锤炼 出来的优秀框架4:多用redismongodb,传统的关系型数据库已经无法市场需求了,这些东西也是面试中的一部分,虽不是重点,但也是加分的选项5:对于微服务和分布式,这个是有一定难度的,我在面试人人车的时候,一面很顺利,二面被技术总监给pass了,问题就是分布式不是特别熟悉!要想进入好的互联网公司,分布式和微服务是很必须的6:jvm的底层,这里要推荐的书就是周志明的《深入jvm虚拟机》这本书了,我总在闲暇时间读它,所以jvm的问题还是信手拈来的最后送上福利:腾讯课堂的的Java工程化、高性能及分布式、高性能、高架构、性能调优、Spring、MyBatis、Netty源码分析视频、java高并发处理视频(在腾讯买得花1800块左右)如果有谁想要,可以私信我,免费分享哦,我花了些钱买的

March 17, 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框架,深入讲解spring的对象的创建

导读项目源码地址因为公司使用的是spring框架,spring是什么?它就像包罗万象的容器,我们什么都可以往里面填,比如集合持久层的hibernate或mybatis框架,类似于拦截器的的shiro框架等等。它的好处是可以自动创建对象。以前,在没有使用spring框架时,我们必须自己创建对象。但自从有了spring框架后,Java开发就像迎来了春天,一切都变的那么简单。它有几种自动创建对象的方式,比如构造器创建对象,set创建对象。。。如果想要对其有更多的了解,那么,下载有很多博客,都对其做了详细的介绍。我在这里不必再做详解了。项目使用了logback和slf4j记录日志信息,因为它们两个是经常合作的。同时,也使用了lombok框架,这个框架可以自动生成set、get、toString、equals、hashcode方法等。下面,便详细介绍我的这个项目。设计模式本项目采用工厂和建造者设计模式。工厂设计模式用来加载配置文件。在没有使用注解的前提下,我们把所有的将要创建对象的信息写进配置文件中,这就是我们常说的依赖注入。而当代码加载时,需要加载这些配置文。这里需要两个雷来支撑。一个是XmlConfigBean,记录每个xml文件中的bean信息。XmlBeanProperty这里记录每个bean中的属性信息。加载文件方法中调用了这两个类,当然,我是用了org下的jdom来读取xml文件,正如以下代码所示。 /** * Created By zby on 22:57 2019/3/4 * 加载配置文件 * * @param dirPath 目录的路径 /public static LoadConfig loadXmlConFig(String dirPath) { if (StringUtils.isEmpty(dirPath)){ throw new RuntimeException(“路径不存在”); } if (null == config) { File dir = new File(dirPath); List<File> files = FactoryBuilder.createFileFactory().listFile(dir); if (CollectionUtil.isEmpty(files)) { throw new RuntimeException(“没有配置文件files=” + files); } allXmls = new HashMap<>(); SAXBuilder saxBuilder = new SAXBuilder(); Document document = null; for (File file : files) { try { Map<String, XmlConfigBean> beanMaps = new HashMap<>(); //创建配置文件 String configFileName = file.getName(); document = saxBuilder.build(file); Element rootEle = document.getRootElement(); List beans = rootEle.getChildren(“bean”); if (CollectionUtil.isNotEmpty(beans)) { int i = 0; for (Iterator beanIterator = beans.iterator(); beanIterator.hasNext(); i++) { Element bean = (Element) beanIterator.next(); XmlConfigBean configBean = new XmlConfigBean(); configBean.setId(attributeToConfigBeanProps(file, i, bean, “id”)); configBean.setClazz(attributeToConfigBeanProps(file, i, bean, “class”)); configBean.setAutowire(attributeToConfigBeanProps(file, i, bean, “autowire”)); configBean.setConfigFileName(configFileName); List properties = bean.getChildren(); Set<XmlBeanProperty> beanProperties = new LinkedHashSet<>(); if (CollectionUtil.isNotEmpty(properties)) { int j = 0; for (Iterator propertyIterator = properties.iterator(); propertyIterator.hasNext(); j++) { Element property = (Element) propertyIterator.next(); XmlBeanProperty beanProperty = new XmlBeanProperty(); beanProperty.setName(attributeToBeanProperty(file, i, j, property, “name”)); beanProperty.setRef(attributeToBeanProperty(file, i, j, property, “ref”)); beanProperty.setValue(attributeToBeanProperty(file, i, j, property, “value”)); beanProperties.add(beanProperty); } configBean.setProperties(beanProperties); } beanMaps.put(configBean.getId(), configBean); } } allXmls.put(configFileName, beanMaps); } catch (JDOMException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return new LoadConfig(); } return config;}上面使用到了文件工厂设计模式,内部使用深度递归算法。如果初始目录下,仍旧有子目录,调用自身的方法,直到遇见文件,如代码所示:/Created By zby on 14:04 2019/2/14获取文件的集合/private void local(File dir) {if (dir == null) { logger.error(“文件夹为空dir=” + dir); throw new RuntimeException(“文件夹为空dir=” + dir);}File[] fies = dir.listFiles();if (ArrayUtil.isNotEmpty(fies)) { for (File fy : fies) { if (fy.isDirectory()) { local(fy); } String fileName = fy.getName(); boolean isMatch = Pattern.compile(reg).matcher(fileName).matches(); boolean isContains = ArrayUtil.containsAny(fileName, FilterConstants.FILE_NAMES); if (isMatch && !isContains) { fileList.add(fy); } }}}建造者设计模式这里用来修饰类信息的。比如,将类名的首字母转化为小写;通过类路径转化为类字面常量,如代码所示: / * Created By zby on 20:19 2019/2/16 * 通过类路径转为类字面常量 * * @param classPath 类路径 /public static <T> Class<T> classPathToClazz(String classPath) { if (StringUtils.isBlank(classPath)) { throw new RuntimeException(“类路径不存在”); } try { return (Class<T>) Class.forName(classPath); } catch (ClassNotFoundException e) { logger.error(“路径” + classPath + “不存在,创建失败e=” + e); e.printStackTrace(); } return null;}类型转换器如果不是用户自定义的类型,我们需要使用类型转化器,将配置文件的数据转化为我们Javabean属性的值。因为,从配置文件读取过来的值,都是字符串类型的,加入Javabean的id为long型,因而,我们需要这个类型转换。/* * Created By zby on 22:31 2019/2/25 * 将bean文件中的value值转化为属性值 /public final class Transfomer { public final static Integer MAX_BYTE = 127; public final static Integer MIN_BYTE = -128; public final static Integer MAX_SHORT = 32767; public final static Integer MIN_SHORT = -32768; public final static String STR_TRUE = “true”; public final static String STR_FALSE = “false”; /* * Created By zby on 22:32 2019/2/25 * 数据转化 * * @param typeName 属性类型的名字 * @param value 值 / public static Object transformerPropertyValue(String typeName, Object value) throws IllegalAccessException { if (StringUtils.isBlank(typeName)) { throw new RuntimeException(“属性的类型不能为空typeName+” + typeName); } if (typeName.equals(StandardBasicTypes.STRING)) { return objToString(value); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.LONG)) { return stringToLong(objToString(value)); } else if (typeName.equals(StandardBasicTypes.INTEGER) || typeName.equals(StandardBasicTypes.INT)) { return stringToInt(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.BYTE)) { return stringToByte(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.SHORT)) { return stringToShort(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.BOOLEAN)) { return stringToBoolean(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.DOUBLE)) { return stringToDouble(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.FLOAT)) { return stringToFloat(objToString(value)); } else if (typeName.equals(StandardBasicTypes.DATE)) { return stringToDate(objToString(value)); } else if (typeName.equals(StandardBasicTypes.BIG_DECIMAL)) { return stringToBigDecimal(objToString(value)); } else { return value; } } /* * Created By zby on 22:32 2019/2/25 * 数据转化 / public static void transformerPropertyValue(Object currentObj, Field field, Object value) throws IllegalAccessException { if (null == currentObj && field == null) { throw new RuntimeException(“当前对象或属性为空值”); } String typeName = field.getType().getSimpleName(); field.setAccessible(true); field.set(currentObj, transformerPropertyValue(typeName, value)); } /* * Created By zby on 23:29 2019/2/25 * obj to String / public static String objToString(Object obj) { return null == obj ? null : obj.toString(); } /* * Created By zby on 23:54 2019/2/25 * String to integer / public static Integer stringToInt(String val) { if (StringUtils.isBlank(val)) { return 0; } if (val.charAt(0) == 0) { throw new RuntimeException(“字符串转为整形失败val=” + val); } return Integer.valueOf(val); } /* * Created By zby on 23:31 2019/2/25 * String to Long / public static Long stringToLong(String val) { return Long.valueOf(stringToInt(val)); } /* * Created By zby on 23:52 2019/2/26 * String to byte / public static Short stringToShort(String val) { Integer result = stringToInt(val); if (result >= MIN_SHORT && result <= MAX_SHORT) { return Short.valueOf(result.toString()); } throw new RuntimeException(“数据转化失败result=” + result); } /* * Created By zby on 0:03 2019/2/27 * String to short / public static Byte stringToByte(String val) { Integer result = stringToInt(val); if (result >= MIN_BYTE && result <= MAX_BYTE) { return Byte.valueOf(result.toString()); } throw new RuntimeException(“数据转化失败result=” + result); } /* * Created By zby on 0:20 2019/2/27 * string to double / public static Double stringToDouble(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败”); } return Double.valueOf(val); } /* * Created By zby on 0:23 2019/2/27 * string to float / public static Float stringToFloat(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败”); } return Float.valueOf(val); } /* * Created By zby on 0:19 2019/2/27 * string to boolean / public static boolean stringToBoolean(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败val=” + val); } if (val.equals(STR_TRUE)) { return true; } if (val.equals(STR_FALSE)) { return false; } byte result = stringToByte(val); if (0 == result) { return false; } if (1 == result) { return true; } throw new RuntimeException(“数据转换失败val=” + val); } /* * Created By zby on 0:24 2019/2/27 * string to Date / public static Date stringToDate(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败val=” + val); } SimpleDateFormat format = new SimpleDateFormat(); try { return format.parse(val); } catch (ParseException e) { throw new RuntimeException(“字符串转为时间失败val=” + val); } } /* * Created By zby on 0:31 2019/2/27 * string to big decimal / public static BigDecimal stringToBigDecimal(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败val=” + val); } return new BigDecimal(stringToDouble(val)); }}常量类型自动装配类型/* * Created By zby on 13:50 2019/2/23 * 装配类型 /public class AutowireType { /* * 缺省情况向,一般通过ref来自动(手动)装配对象 / public static final String NONE = null; /* * 根据属性名事项自动装配, * 如果一个bean的名称和其他bean属性的名称是一样的,将会自装配它。 / public static final String BY_NAME = “byName”; /* * 根据类型来装配 * 如果一个bean的数据类型是用其它bean属性的数据类型,兼容并自动装配它。 / public static final String BY_TYPE = “byType”; /* * 根据构造器constructor创建对象 / public static final String CONSTRUCTOR = “constructor”; /* * autodetect – 如果找到默认的构造函数,使用“自动装配用构造”; 否则,使用“按类型自动装配”。 / public static final String AUTODETECT = “autodetect”; }属性类型常量池/* * Created By zby on 22:44 2019/2/25 * 类型常量池 /public class StandardBasicTypes { public static final String STRING = “String”; public static final String LONG = “Long”; public static final String INTEGER = “Integer”; public static final String INT = “int”; public static final String BYTE = “Byte”; public static final String SHORT = “Short”; public static final String BOOLEAN = “Boolean”; public static final String DOUBLE = “double”; public static final String FLOAT = “float”; public static final String DATE = “Date”; public static final String TIMESTAMP = “Timestamp”; public static final String BIG_DECIMAL = “BigDecimal”; public static final String BIG_INTEGER = “BigInteger”;}getBean加载上下文文件首先需要一个构造器,形参时文件的名字;getBean方法,形参是某个bean的id名字,这样,根据当前bean的自动装配类型,来调用响应的方法。 /* * Created By zby on 11:17 2019/2/14 * 类的上下文加载顺序 /public class ClassPathXmlApplicationContext {private static Logger logger = LoggerFactory.getLogger(ClassPathXmlApplicationContext.class.getName());private String configXml;public ClassPathXmlApplicationContext(String configXml) { this.configXml = configXml;}/* * Created By zby on 18:38 2019/2/24 * bean对应的id的名称 /public Object getBean(String name) { String dirPath="../simulaspring/src/main/resources/"; Map<String, Map<String, XmlConfigBean>> allXmls = LoadConfig.loadXmlConFig(dirPath).getAllXmls(); boolean contaninsKey = MapUtil.findKey(allXmls, configXml); if (!contaninsKey) { throw new RuntimeException(“配置文件不存在” + configXml); } Map<String, XmlConfigBean> beans = allXmls.get(configXml); contaninsKey = MapUtil.findKey(beans, name); if (!contaninsKey) { throw new RuntimeException(“id为” + name + “bean不存在”); } XmlConfigBean configFile = beans.get(name); if (null == configFile) { throw new RuntimeException(“id为” + name + “bean不存在”); } String classPath = configFile.getClazz(); if (StringUtils.isBlank(classPath)) { throw new RuntimeException(“id为” + name + “类型不存在”); } String autowire = configFile.getAutowire(); if (StringUtils.isBlank(autowire)) { return getBeanWithoutArgs(beans, classPath, configFile); } else { switch (autowire) { case AutowireType.BY_NAME: return getBeanByName(beans, classPath, configFile); case AutowireType.CONSTRUCTOR: return getBeanByConstruct(classPath, configFile); case AutowireType.AUTODETECT: return getByAutodetect(beans, classPath, configFile); case AutowireType.BY_TYPE: return getByType(beans, classPath, configFile); } } return null; }}下面主要讲解默认自动装配、属性自动装配、构造器自动装配默认自动装配如果我们没有填写自动装配的类型,其就采用ref来自动(手动)装配对象。 /* * Created By zby on 18:33 2019/2/24 * 在没有设置自动装配时,通过ref对象 /private Object getBeanWithoutArgs(Map<String, XmlConfigBean> beans, String classPath, XmlConfigBean configFile) {//属性名称String proName = null;try { Class currentClass = Class.forName(classPath); //通过引用 ref 创建对象 Set<XmlBeanProperty> properties = configFile.getProperties(); //如果没有属性,就返回,便于下面的递归操作 if (CollectionUtil.isEmpty(properties)) { return currentClass.newInstance(); } Class<?> superClass = currentClass.getSuperclass(); //TODO 父类的集合// List<Class> superClasses = null; //在创建子类构造器之前,创建父类构造器, // 父类构造器的参数子类构造器的参数 Object currentObj = null; //当前构造器 Object consArgsObj = null; String consArgsName = null; boolean hasSuperClass = (null != superClass && !superClass.getSimpleName().equals(“Object”)); if (hasSuperClass) { Constructor[] constructors = currentClass.getDeclaredConstructors(); ArrayUtil.validateArray(superClass, constructors); Parameter[] parameters = constructors[0].getParameters(); if (parameters == null || parameters.length == 0) { consArgsObj = constructors[0].newInstance(); } else { ArrayUtil.validateArray(superClass, parameters); consArgsName = parameters[0].getType().getSimpleName(); //配置文件大类型,与参数构造器的类型是否相同 for (XmlBeanProperty property : properties) { String ref = property.getRef(); if (StringUtils.isNotBlank(ref) && ref.equalsIgnoreCase(consArgsName)) { classPath = beans.get(ref).getClazz(); Class<?> clazz = Class.forName(classPath); consArgsObj = clazz.newInstance(); } } currentObj = constructors[0].newInstance(consArgsObj); } } else { currentObj = currentClass.newInstance(); } for (XmlBeanProperty property : properties) { //这里适合用递归,无限调用自身 //通过name找到属性,配置文件中是否有该属性,通过ref找到其对应的bean文件 proName = property.getName(); Field field = currentClass.getDeclaredField(proName); if (null != field) { String ref = property.getRef(); Object value = property.getValue(); //如果没有赋初值,就通过类型创建 if (null == value && StringUtils.isNotBlank(ref)) { boolean flag = StringUtils.isNotBlank(consArgsName) && null != consArgsObj && consArgsName.equalsIgnoreCase(ref); //递归调用获取属性对象 value = flag ? consArgsObj : getBean(ref); } field.setAccessible(true); Transfomer.transformerPropertyValue(currentObj, field, value); } } return currentObj;} catch (ClassNotFoundException e) { logger.error(“名为” + classPath + “类不存在”); e.printStackTrace();} catch (InstantiationException e) { e.printStackTrace();} catch (IllegalAccessException e) { e.printStackTrace();} catch (InvocationTargetException e) { e.printStackTrace();} catch (NoSuchFieldException e) { logger.error(classPath + “类的属性” + proName + “不存在”); throw new RuntimeException(classPath + “类的属性” + proName + “不存在”);}return null;}构造器创建对象根据构造器constructor创建对象/* * Created By zby on 23:06 2019/3/2 * * @param classPath 类路径 * @param configFile 配置文件 /private Object getBeanByConstruct(String classPath, XmlConfigBean configFile) { try { Class currentClass = Class.forName(classPath); Set<XmlBeanProperty> properties = configFile.getProperties(); if (CollectionUtil.isEmpty(properties)) { return currentClass.newInstance(); } ///构造器参数类型和构造器对象集合 Object[] objects = new Object[properties.size()]; Class<?>[] paramType = new Class[properties.size()]; Field[] fields = currentClass.getDeclaredFields(); int i = 0; for (Iterator iterator = properties.iterator(); iterator.hasNext(); i++) { XmlBeanProperty property = (XmlBeanProperty) iterator.next(); String proName = property.getName(); String ref = property.getRef(); Object value = property.getValue(); for (Field field : fields) { Class<?> type = field.getType(); String typeName = type.getSimpleName(); String paramName = field.getName(); if (paramName.equals(proName) && ObjectUtil.isNotNull(value) && StringUtils.isBlank(ref)) { objects[i] = Transfomer.transformerPropertyValue(typeName, value); paramType[i] = type; break; } else if (paramName.equals(proName) && StringUtils.isNotBlank(ref) && ObjectUtil.isNull(value)) { objects[i] = getBean(ref); paramType[i] = type; break; } } } return currentClass.getConstructor(paramType).newInstance(objects); } catch (ClassNotFoundException e) { logger.error(“名为” + classPath + “类不存在”); e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } return null;}属性自动装配根据属性名事项自动装配,如果一个bean的名称和其他bean属性的名称是一样的,将会自装配它。 /* * Created By zby on 21:16 2019/3/1 * 根据属性名事项自动装配, * @param classPath 类路径 * @param configFile 配置文件 */private Object getBeanByName( String classPath, XmlConfigBean configFile) { String proName = null; try { Class currentClass = Class.forName(classPath); Class superclass = currentClass.getSuperclass(); Method[] methods = currentClass.getDeclaredMethods(); List<Method> methodList = MethodHelper.filterSetMethods(methods); Object currentObj = currentClass.newInstance(); Set<XmlBeanProperty> properties = configFile.getProperties(); //配置文件中,但是有父类, if (CollectionUtil.isEmpty(properties)) { boolean isExit = null != superclass && !superclass.getSimpleName().equals(“Object”); if (isExit) { Field[] parentFields = superclass.getDeclaredFields(); if (ArrayUtil.isNotEmpty(parentFields)) { if (CollectionUtil.isNotEmpty(methodList)) { for (Field parentField : parentFields) { for (Method method : methodList) { if (MethodHelper.methodNameToProName(method.getName()).equals(parentField.getName())) { //如果有泛型的话 Type genericType = currentClass.getGenericSuperclass(); if (null != genericType) { String genericName = genericType.getTypeName(); genericName = StringUtils.substring(genericName, genericName.indexOf("<") + 1, genericName.indexOf(">")); Class genericClass = Class.forName(genericName); method.setAccessible(true); method.invoke(currentObj, genericClass); } break; } } break; } } } } return currentObj; } //传递给父级对象 service – 》value List<Method> tmpList = new ArrayList<>(); Map<String, Object> map = new HashMap<>(); Object value = null; for (XmlBeanProperty property : properties) { proName = property.getName(); if (ArrayUtil.isNotEmpty(methods)) { String ref = property.getRef(); value = property.getValue(); for (Method method : methodList) { String methodName = MethodHelper.methodNameToProName(method.getName()); Field field = currentClass.getDeclaredField(methodName); if (methodName.equals(proName) && null != field) { if (null == value && StringUtils.isNotBlank(ref)) { value = getBean(ref); } else if (value != null && StringUtils.isBlank(ref)) { value = Transfomer.transformerPropertyValue(field.getType().getSimpleName(), value); } method.setAccessible(true); method.invoke(currentObj, value); map.put(proName, value); tmpList.add(method); break; } } } } tmpList = MethodHelper.removeMethod(methodList, tmpList); for (Method method : tmpList) { Class<?>[] type = method.getParameterTypes(); if (ArrayUtil.isEmpty(type)) { throw new RuntimeException(“传递给父级对象的参数为空type=” + type); } for (Class<?> aClass : type) { String superName = ClassHelper.classNameToProName(aClass.getSimpleName()); value = map.get(superName); method.setAccessible(true); method.invoke(currentObj, value); } } return currentObj; } catch (ClassNotFoundException e) { logger.error(“名为” + classPath + “类不存在”); e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { logger.error(“类” + classPath + “属性” + proName + “不存在”); e.printStackTrace(); } return null;}总结这里没有使用注解,我们可以使用注解的方式实现自动装配,但这不spring的核心,应该时spring的美化,核心值如何实现自动装配。 ...

March 17, 2019 · 10 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 MVC之基于java config无xml配置的web应用构建

更多spring相关博文参考: http://spring.hhui.top前一篇博文讲了SpringMVC+web.xml的方式创建web应用,用过SpringBoot的童鞋都知道,早就没有xml什么事情了,其实Spring 3+, Servlet 3+的版本,就已经支持java config,不用再写xml;本篇将介绍下,如何利用java config取代xml配置本篇博文,建议和上一篇对比看,贴出上一篇地址190316-Spring MVC之基于xml配置的web应用构建<!– more –>I. Web构建1. 项目依赖对于依赖这一块,和前面一样,不同的在于java config 取代 xml<artifactId>200-mvc-annotation</artifactId><packaging>war</packaging><properties> <spring.version>5.1.5.RELEASE</spring.version></properties><dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.eclipse.jetty.aggregate</groupId> <artifactId>jetty-all</artifactId> <version>9.2.19.v20160908</version> </dependency></dependencies><build> <finalName>web-mvc</finalName> <plugins> <plugin> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>9.4.12.RC2</version> <configuration> <httpConnector> <port>8080</port> </httpConnector> </configuration> </plugin> </plugins></build>细心的童鞋会看到,依赖中多了一个jetty-all,后面测试篇幅会说到用法2. 项目结构第二节依然放上项目结构,在这里把xml的结构也截进来了,对于我们的示例demo而言,最大的区别就是没有了webapp,更没有webapp下面的几个xml配置文件3. 配置设定现在没有了配置文件,我们的配置还是得有,不然web容器(如tomcat)怎么找到DispatchServlet呢a. DispatchServlet 声明同样我们需要干的第一件事情及时声明DispatchServlet,并设置它的应用上下文;可以怎么用呢?从官方找到教程{% blockquote @SpringWebMvc教程 https://docs.spring.io/spring… %}The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification by using Java configuration or in web.xml. In turn, the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling{% endblockquote %}上面的解释,就是说下面的代码和web.xml的效果是一样一样的public class MyWebApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletCxt) { // Load Spring web application configuration AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext(); ac.register(AppConfig.class); ac.refresh(); // Create and register the DispatcherServlet DispatcherServlet servlet = new DispatcherServlet(ac); ServletRegistration.Dynamic registration = servletCxt.addServlet(“mvc-dispatcher”, servlet); registration.setLoadOnStartup(1); registration.addMapping("/"); }}当然直接实现接口的方式有点粗暴,但是好理解,上面的代码和我们前面的web.xml效果一样,创建了一个DispatchServlet, 并且绑定了url命中规则;设置了应用上下文AnnotationConfigWebApplicationContext这个上下文,和我们前面的配置文件mvc-dispatcher-servlet有点像了;如果有兴趣看到项目源码的同学,会发现用的不是上面这个方式,而是及基础接口AbstractDispatcherServletInitializerpublic class MyWebApplicationInitializer extends AbstractDispatcherServletInitializer { @Override protected WebApplicationContext createRootApplicationContext() { return null; } @Override protected WebApplicationContext createServletApplicationContext() { AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); // applicationContext.setConfigLocation(“com.git.hui.spring”); applicationContext.register(RootConfig.class); applicationContext.register(WebConfig.class); return applicationContext; } @Override protected String[] getServletMappings() { return new String[]{"/"}; } @Override protected Filter[] getServletFilters() { return new Filter[]{new HiddenHttpMethodFilter(), new CharacterEncodingFilter()}; }}看到上面这段代码,这个感觉就和xml的方式更像了,比如Servlet应用上下文和根应用上下文说明上面代码中增加的Filter先无视,后续会有专文讲什么是Filter以及Filter可以怎么用b. java config前面定义了DispatchServlet,接下来对比web.xml就是需要配置扫描并注册bean了,本文基于JavaConfig的方式,则主要是借助 @Configuration 注解来声明配置类(这个可以等同于一个xml文件)前面的代码也可以看到,上下文中注册了两个Config类RootConfig定义如下,注意下注解@ComponentScan,这个等同于<context:component-sca/>,指定了扫描并注册激活的bean的包路径@Configuration@ComponentScan(value = “com.git.hui.spring”)public class RootConfig {}另外一个WebConfig的作用则主要在于开启WebMVC@Configuration@EnableWebMvcpublic class WebConfig implements WebMvcConfigurer {}4. 实例代码实例和上一篇一样,一个普通的Server Bean和一个Controller@Componentpublic class PrintServer { public void print() { System.out.println(System.currentTimeMillis()); }}一个提供rest服务的HelloRest@RestControllerpublic class HelloRest { @Autowired private PrintServer printServer; @GetMapping(path = “hello”, produces=“text/html;charset=UTF-8”) public String sayHello(HttpServletRequest request) { printServer.print(); return “hello, " + request.getParameter(“name”); } @GetMapping({”/", “”}) public String index() { return UUID.randomUUID().toString(); }}5. 测试测试依然可以和前面一样,使用jetty来启动,此外,介绍另外一种测试方式,也是jetty,但是不同的是我们直接写main方法来启动服务public class SpringApplication { public static void main(String[] args) throws Exception { Server server = new Server(8080); ServletContextHandler handler = new ServletContextHandler(); // 服务器根目录,类似于tomcat部署的项目。 完整的访问路径为ip:port/contextPath/realRequestMapping //ip:port/项目路径/api请求路径 handler.setContextPath("/"); AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); applicationContext.register(WebConfig.class); applicationContext.register(RootConfig.class); //相当于web.xml中配置的ContextLoaderListener handler.addEventListener(new ContextLoaderListener(applicationContext)); //springmvc拦截规则 相当于web.xml中配置的DispatcherServlet handler.addServlet(new ServletHolder(new DispatcherServlet(applicationContext)), “/*”); server.setHandler(handler); server.start(); server.join(); }}测试示意图如下6. 小结简单对比下xml的方式,会发现java config方式会清爽很多,不需要多个xml配置文件,维持几个配置类,加几个注解即可;当然再后面的SpringBoot就更简单了,几个注解了事,连上面的两个Config文件, ServletConfig都可以省略掉另外一个需要注意的点就是java config的运行方式,在servlet3之后才支持的,也就是说如果用比较老的jetty是起不来的(或者无法正常访问web服务)II. 其他- 系列博文web系列:Spring Web系列博文汇总mvc应用搭建篇:190316-Spring MVC之基于xml配置的web应用构建190317-Spring MVC之基于java config无xml配置的web应用构建0. 项目工程:spring-boot-demo项目: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring/200-mvc-annotation1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

March 17, 2019 · 2 min · jiezi

Spring MVC之基于xml配置的web应用构建

> 更多spring博文参考: http://spring.hhui.top/直接用SpringBoot构建web应用可以说非常非常简单了,在使用SpringBoot构建后端服务之前,一直用的是Spring + SpringMVC基于xml的配置方式来玩的,所以在正式进入SpringBoot Web篇之前,有必要看一下不用SpringBoot应该怎么玩的,也因此方便凸显SpringBoot的优越性<!– more –>I. Web 构建1. 项目依赖我们选择使用传统的SpringMVC + Tomcat/Jetty 运行war包方式来运行任务,创建一个maven项目之后,先添加上基本的依赖<artifactId>201-mvc-xml</artifactId><!– 注意这一行,我们指定war包 –><packaging>war</packaging><properties> <spring.version>5.1.5.RELEASE</spring.version></properties><dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency></dependencies>2. 项目结构对于web项目,和我们传统的不一样的地方在于,会多一个 webapp 目录,在这个目录的 WEB-INF 文件夹下,会存有几个必要的配置文件图中的三个目录,都属于比较重要的java : 存放源码resources: 项目资源文件存放地webapp: web的配置文件,资源文件默认存放地3. 配置文件说明java和resources这两个目录没啥好说的,主要来看一下webapp下面的三个xml配置文件a. web.xml在我们使用xml配置的生态体系中,这个配置文件至关重要;本节说到SpringMVC构建的应用,是在Servlet的生态上玩耍的;而web.xml这个配置文件,比如我们常见的Servlet定义,filter定义等等,都在这xml文件中实例如下<?xml version=“1.0” encoding=“UTF-8”?><web-app xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation=“http://xmlns.jcp.org/xml/ns/javaeehttp://java.sun.com/xml/ns/j2ee/web-app_3_1.xsd" version=“3.1”> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <!– 解决乱码的问题 –> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <async-supported>true</async-supported> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>mvc-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>mvc-dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping></web-app>上面的配置中,定义了 DispatcherServlet的名字为 mvc-dispatcher,根据规范,会有一个叫做 mvc-dispatcher-servlet.xml的配置文件,其中的配置将应用于DispatcherServlet的上下文b. mvc-dispatcher-servlet.xml这个文件主要可以用来定义Servlet相关的配置信息,比如视图解析,资源路径指定等;一个最简单的配置如下<beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns:aop=“http://www.springframework.org/schema/aop" xmlns:context=“http://www.springframework.org/schema/context" xmlns:beans=“http://www.springframework.org/schema/mvc" xmlns=“http://www.springframework.org/schema/beans" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!–指定扫描的包路径,自动注册包含指定注解的对象到Spring容器,并包含了 context:annotation-config 的作用–> <context:component-scan base-package=“com.git.hui.spring”/></beans>在web.xml中,context:component-scan非常非常重要,用来指定自动扫描并注册bean到容器的包路径,上面这一行配置,简单来讲可以认为做了下面几件事情扫描包 com.git.hui.spring 下所有的类,如果类上有 @Component, @Service, @Repository, @Contorller, @RestContorller, @Configuration等注解,会实例化为bean对象,并注册到Spring容器中其次就是实现DI的功能,实现bean的依赖注入接下来看一下,如果不加上面这一行,也想实现对应的效果改怎样配置呢?<!– 这个使用来激活注册的Bean,简单来讲就是使Ioc工作起来 –><context:annotation-config/><bean name=“printServer” class=“com.git.hui.spring.PrintServer”/><bean name=“helloRest” class=“com.git.hui.spring.HelloRest”/>源码后面会给出,首先是主动定义两个bean,其中 helloRest 为Controller, printServer 为一个Service,并被注入到helloRest中如果只定义了两个bean,而不加上<context:annotation-config/>,则HelloRest中的printService会是null,演示如下图此外,如果用了旧的Spring版本,直接用前面的配置,可能依然无法访问web服务,这个时候有必要加一下下面的注解; 对于使用aop,希望使用cglib代理的,需要如下配置<!– 支持mvc注解–><mvc:annotation-driven/><!– 使用cglib实现切面代理 –><aop:aspectj-autoproxy proxy-target-class=“true”/>额外说明:现在基本上不怎么用xml配置了,有更简单的注解方式,上面的配置内容了解即可c. applicationContext.xml前面的截图中,还有个配置文件,这个是干嘛的呢?DispatchServlet加载包含在web组件中的bean(如mapper,Controller,ViewResolver);我们应用中,还有些其他的Spring Bean(比如其他rpc访问的服务bean代理,db驱动组件等)则更多的是放在这个配置文件中定义当然这个里面最简单的配置内容就是啥都没有,比如我们的demo工程<beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"></beans>4. 实例代码配置完了之后,我们简单的定义一个reset服务用来测试,比如一个简单dean对象和一个简单的Controller简单的bean对象@Componentpublic class PrintServer { public void print() { System.out.println(System.currentTimeMillis()); }}Controller如下@RestControllerpublic class HelloRest { @Autowired private PrintServer printServer; @ResponseBody @GetMapping(“hello”) public String sayHello(HttpServletRequest request) { printServer.print(); return “hello, " + request.getParameter(“name”); }}5. 测试上面我们的web应用就搭建完毕了,然后就是把它部署起来,看下能不能愉快的玩耍了;我们有两个方法方法一:tomcat方式打包 mvn clean package -DskipTests=true ,然后target目录下会生成一个war包将war包放在tomcat的webapps目录下,然后启动tomcat进行访问即可方法二:jetty方式前面一种方式,有很多公司的服务是这么玩的,将服务达成war包丢到tomcat中,然后服务上线;然而在本地开发测试时,这样有点麻烦(当然可以通过idea配置tomcat调试法,个人感觉,依然麻烦)我们使用jetty来玩耍就很简单了,首先在pom中添加配置,引入jetty插件<build> <finalName>web-mvc</finalName> <plugins> <plugin> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>9.4.12.RC2</version> <configuration> <httpConnector> <port>8080</port> </httpConnector> </configuration> </plugin> </plugins></build>然后启动方式可以使用命令: mvn jetty:run, 也可以使用idea,如下,直接双击运行或者右键选择debug模式启动然后我们愉快的启动测试过程如下到此,一个基于 Spring + SpringMVC + Jetty + xml配置的web应用就搭建起来了;下一篇我们将讲一下,纯java注解方式,抛弃xml配置又可以怎样搭建一个web应用II. 其他- 系列博文web系列:Spring Web系列博文汇总mvc应用搭建篇:190316-Spring MVC之基于xml配置的web应用构建190317-Spring MVC之基于java config无xml配置的web应用构建0. 项目工程:https://github.com/liuyueyi/spring-boot-demo项目: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring/201-mvc-xml1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

March 17, 2019 · 2 min · jiezi

Java 高并发环境下的性能优化,揭秘支付宝技术内幕

前言高并发经常会发生在有大活跃用户量,用户高聚集的业务场景中,如:秒杀活动,定时领取红包等。为了让业务可以流畅的运行并且给用户一个好的交互体验,我们需要根据业务场景预估达到的并发量等因素,来设计适合自己业务场景的高并发处理方案。在电商相关产品开发的这些年,我有幸的遇到了并发下的各种坑,这一路摸爬滚打过来有着不少的血泪史,这里进行的总结,作为自己的归档记录,同时分享给大家。Java 高并发环境下的性能优化(附高并发视频资料和面试题等),揭秘支付宝技术内幕 私信。服务器架构业务从发展的初期到逐渐成熟,服务器架构也是从相对单一到集群,再到分布式服务。一个可以支持高并发的服务少不了好的服务器架构,需要有均衡负载,数据库需要主从集群,nosql缓存需要主从集群,静态文件需要上传cdn,这些都是能让业务程序流畅运行的强大后盾。服务器这块多是需要运维人员来配合搭建,具体我就不多说了,点到为止。大致需要用到的服务器架构如下:服务器均衡负载(如:nginx,阿里云SLB)资源监控分布式数据库主从分离,集群DBA 表优化,索引优化,等分布式nosql主从分离,集群主从分离,集群主从分离,集群redismongodbmemcachecdnhtmlcssjsimage并发测试高并发相关的业务,需要进行并发的测试,通过大量的数据分析评估出整个架构可以支撑的并发量。测试高并发可以使用第三方服务器或者自己测试服务器,利用测试工具进行并发请求测试,分析测试数据得到可以支撑并发数量的评估,这个可以作为一个预警参考,俗话说知己自彼百战不殆。第三方服务:阿里云性能测试并发测试工具:Apache JMeterVisual Studio性能负载测试Microsoft Web Application Stress Tool实战方案通用方案日用户流量大,但是比较分散,偶尔会有用户高聚的情况;场景: 用户签到,用户中心,用户订单,等服务器架构图:说明:场景中的这些业务基本是用户进入APP后会操作到的,除了活动日(618,双11,等),这些业务的用户量都不会高聚集,同时这些业务相关的表都是大数据表,业务多是查询操作,所以我们需要减少用户直接命中DB的查询;优先查询缓存,如果缓存不存在,再进行DB查询,将查询结果缓存起来。更新用户相关缓存需要分布式存储,比如使用用户ID进行hash分组,把用户分布到不同的缓存中,这样一个缓存集合的总量不会很大,不会影响查询效率。方案如:用户签到获取积分计算出用户分布的key,redis hash中查找用户今日签到信息如果查询到签到信息,返回签到信息如果没有查询到,DB查询今日是否签到过,如果有签到过,就把签到信息同步redis缓存。如果DB中也没有查询到今日的签到记录,就进行签到逻辑,操作DB添加今日签到记录,添加签到积分(这整个DB操作是一个事务)缓存签到信息到redis,返回签到信息注意这里会有并发情况下的逻辑问题,如:一天签到多次,发放多次积分给用户。用户订单这里我们只缓存用户第一页的订单信息,一页40条数据,用户一般也只会看第一页的订单数据用户访问订单列表,如果是第一页读缓存,如果不是读DB计算出用户分布的key,redis hash中查找用户订单信息如果查询到用户订单信息,返回订单信息如果不存在就进行DB查询第一页的订单数据,然后缓存redis,返回订单信息用户中心计算出用户分布的key,redis hash中查找用户订单信息如果查询到用户信息,返回用户信息如果不存在进行用户DB查询,然后缓存redis,返回用户信息其他业务上面例子多是针对用户存储缓存,如果是公用的缓存数据需要注意一些问题,如下注意公用的缓存数据需要考虑并发下的可能会导致大量命中DB查询,可以使用管理后台更新缓存,或者DB查询的锁住操作。以上例子是一个相对简单的高并发架构,并发量不是很高的情况可以很好的支撑,但是随着业务的壮大,用户并发量增加,我们的架构也会进行不断的优化和演变,比如对业务进行服务化,每个服务有自己的并发架构,自己的均衡服务器,分布式数据库,nosql主从集群,如:用户服务、订单服务;消息队列秒杀、秒抢等活动业务,用户在瞬间涌入产生高并发请求场景:定时领取红包,等服务器架构图:一级缓存高并发请求连接缓存服务器超出服务器能够接收的请求连接量,部分用户出现建立连接超时无法读取到数据的问题;因此需要有个方案当高并发时候时候可以减少命中缓存服务器;这时候就出现了一级缓存的方案,一级缓存就是使用站点服务器缓存去存储数据,注意只存储部分请求量大的数据,并且缓存的数据量要控制,不能过分的使用站点服务器的内存而影响了站点应用程序的正常运行,一级缓存需要设置秒单位的过期时间,具体时间根据业务场景设定,目的是当有高并发请求的时候可以让数据的获取命中到一级缓存,而不用连接缓存nosql数据服务器,减少nosql数据服务器的压力比如APP首屏商品数据接口,这些数据是公共的不会针对用户自定义,而且这些数据不会频繁的更新,像这种接口的请求量比较大就可以加入一级缓存;服务器架构图:静态化数据高并发请求数据不变化的情况下如果可以不请求自己的服务器获取数据那就可以减少服务器的资源压力。对于更新频繁度不高,并且数据允许短时间内的延迟,可以通过数据静态化成JSON,XML,HTML等数据文件上传CDN,在拉取数据的时候优先到CDN拉取,如果没有获取到数据再从缓存,数据库中获取,当管理人员操作后台编辑数据再重新生成静态文件上传同步到CDN,这样在高并发的时候可以使数据的获取命中在CDN服务器上。CDN节点同步有一定的延迟性,所以找一个靠谱的CDN服务器商也很重要

March 17, 2019 · 1 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 + swagger自动生成HTML、PDF接口文档,并解决中文显示为空白问题

做后端开发,自然离不开接口文档,接口文档不仅方便后端开发人员之间查看,更是前端人员必要的文档,也有可能提供给第三方来调用我们的接口。但是,写接口文档太费时间,而且如果没有确定好格式,每个人写的接口文档可能各不相同,看起来就会很混乱。好在swagger出现了,如果你的spring boot项目集成了swagger,而且接口和入参出参实体类加上了swagger相关的注解(参考最终demo中的controller和model),那么,就可以通过http://ip:port/swagger-ui.html(ip和port换成自己配置的)来访问在线的接口,在此页面也可以直接测试接口。对spring boot和swagger不了解的建议先学习一下,近年来很火,使用起来也确实方便。但是我们肯定不会满足在线访问就可以了的,有时候会需要离线的接口文档,于是就有了swagger2markup、springFox、asciidoctor几个插件来帮助我们生成离线的HTML和PDF格式的文档。关于使用swagger生成HTML或者PDF的原理,可以参考这篇文章:使用 SpringFox、Swagger2Markup、Spring-Restdoc和 Maven 构建 RESTful API文档。首先是从spring-swagger2markup-demo下载了demo,这个demo已经能够生成HTML和PDF文档了,但是对中文支持不好,中文大部分会显示为空白。如果你的接口文档是全英文的,那么就用这个就可以了。关于这个demo对中文支持不好,查了很多资料,应该是字体和主题的原因,所以参考了很多资料,结合当前这个demo,做出了最终的能很好支持中文的demo,最终demo地址:swagger2pdf。生成的文档存放的目录:当前项目的target\asciidoc\html和target\asciidoc\pdf分别存放着HTML文档和PDF文档。关于接口和入参出参实体类中用到的swagger注解,可以参考这篇博客:swagger2常用注解说明。最终生成的HTML文档和PDF文档效果图:由于参考了很多资料都没有成功,只记录了最后成功的链接,没有记录下其他的链接,如果您觉得其中有参考您的部分,可以留言留下您的地址,我会加到参考的链接里的。主要参考:https://github.com/Swagger2Ma…https://blog.csdn.net/lihuaij...https://github.com/woshihouji…

March 16, 2019 · 1 min · jiezi

Lombok安装及Spring Boot集成Lombok

Lombok有什么用在我们实体Bean中有大量的Getter/Setter方法以及toString, hashCode等可能不会用到,但是某些时候仍然需要复写;在使用Lombok之后,将由其来自动帮你实现代码生成。注意,其是在编译源码过程中,帮你自动生成的。就是说,将极大减少你的代码总量。Lombok的官方地址: https://projectlombok.org/使用Lombok时需要注意的点在类需要序列化、反序列化时或者需要详细控制字段时,应该谨慎考虑是否要使用Lombok,因为在这种情况下容易出问题。例如:Jackson、Json序列化使用Lombok虽然能够省去手动创建setter和getter方法等繁琐事情,但是却降低了源代码文件的可读性和完整性,减低了阅读源代码的舒适度使用@Slf4j还是@Log4j注解,需要根据实际项目中使用的日志框架来选择。Lombok并非处处适用,我们需要选择适合的地方使用Lombok,例如pojo是一个好地方,因为pojo很单纯Lombok的安装eclipse安装Lombok步骤:下载最新的lombok.jar包,下载地址:https://projectlombok.org/download.html进入cmd窗口,切到Lombok下载的目录,运行命令: java -jar lombok.jar,会出现如下界面:已经默认选好了eclipse安装目录(这个可能是因为我只有一个盘,如果没有默认选择,可以自己点击下方Specify location…按钮选择eclipse安装目录),点击图中红色箭头指向的按钮,即可完成安装。成功界面如下:eclipse安装目录下的eclipse.ini文件末尾已经加了一行内容(这个路径因人而异,和eclipse安装目录有关),如下:而且安装目录下也多了一个lombok.jarspring boot集成Lombok先去http://start.spring.io/在线生成一个spring boot项目脚手架,导入eclipse。在pom.xml里添加Lombok依赖:<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.14</version></dependency>在src/main/java/com/example/springbootlombok/entity下新建一个student.java的Java bean:package com.example.springbootlombok.entity;import lombok.Data; @Datapublic class Student { private String name; private int age;}在src/test/java/com/example/springbootlombok下新建一个TestEntity.java的测试类:package com.example.springbootlombok;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner; import com.example.springbootlombok.entity.Student; import lombok.extern.slf4j.Slf4j; @RunWith(SpringRunner.class)@SpringBootTest@Slf4jpublic class TestEntity { Student student = new Student(); @Test public void test() { student.setName(“张三”); student.setAge(12); log.info(“测试结果:” + student.toString()); }}执行JUnit测试,成功的话,日志里会有打印测试结果:Student(name=张三, age=12),至此,spring boot已经成功集成Lombok了。Lombok常用注解@NonNull这个注解可以用在成员方法或者构造方法的参数前面,会自动产生一个关于此参数的非空检查,如果参数为空,则抛出一个空指针异常,举个例子:编译前的代码://成员方法参数加上@NonNull注解public String getName(@NonNull Person p) { return p.getName();}编译后的代码:public String getName(@NonNull Person p) { if (p == null) { throw new NullPointerException(“person”); } return p.getName();}@Cleanup这个注解用在变量前面,可以保证此变量代表的资源会被自动关闭,默认是调用资源的close()方法,如果该资源有其它关闭方法,可使用@Cleanup(“methodName”)来指定要调用的方法,就用输入输出流来举个例子:编译前的代码:public static void main(String[] args) throws IOException { @Cleanup InputStream in = new FileInputStream(args[0]); @Cleanup OutputStream out = new FileOutputStream(args[1]); byte[] b = new byte[1024]; while (true) { int r = in.read(b); if (r == -1) break; out.write(b, 0, r); } }编译后的代码:public static void main(String[] args) throws IOException { InputStream in = new FileInputStream(args[0]); try { OutputStream out = new FileOutputStream(args[1]); try { byte[] b = new byte[10000]; while (true) { int r = in.read(b); if (r == -1) break; out.write(b, 0, r); } } finally { if (out != null) { out.close(); } } } finally { if (in != null) { in.close(); } }}@Getter/@Setter这一对注解从名字上就很好理解,用在成员变量前面,相当于为成员变量生成对应的get和set方法,同时还可以为生成的方法指定访问修饰符,当然,默认为public,直接来看下面的简单的例子:编译前的代码:public class Programmer { @Getter @Setter private String name; @Setter(AccessLevel.PROTECTED) private int age; @Getter(AccessLevel.PUBLIC) private String language;}编译后的代码:public class Programmer { private String name; private int age; private String language; public void setName(String name) { this.name = name; } public String getName() { return name; } protected void setAge(int age) { this.age = age; } public String getLanguage() { return language; }}这两个注解还可以直接用在类上,可以为此类里的所有非静态成员变量生成对应的get和set方法。@Getter(lazy=true)如果Bean的一个字段的初始化是代价比较高的操作,比如加载大量的数据;同时这个字段并不是必定使用的。那么使用懒加载机制,可以保证节省资源。懒加载机制,是对象初始化时,该字段并不会真正的初始化,而是第一次访问该字段时才进行初始化字段的操作。@ToString/@EqualsAndHashCode这两个注解也比较好理解,就是生成toString,equals和hashcode方法,同时后者还会生成一个canEqual方法,用于判断某个对象是否是当前类的实例,生成方法时只会使用类中的非静态和非transient成员变量,这些都比较好理解,就不举例子了。当然,这两个注解也可以添加限制条件,例如用@ToString(exclude={“param1”,“param2”})来排除param1和param2两个成员变量,或者用@ToString(of={“param1”,“param2”})来指定使用param1和param2两个成员变量,@EqualsAndHashCode注解也有同样的用法。@NoArgsConstructor/@RequiredArgsConstructor /@AllArgsConstructor这三个注解都是用在类上的,第一个和第三个都很好理解,就是为该类产生无参的构造方法和包含所有参数的构造方法,第二个注解则使用类中所有带有@NonNull注解的或者带有final修饰的成员变量生成对应的构造方法。当然,和前面几个注解一样,成员变量都是非静态的,另外,如果类中含有final修饰的成员变量,是无法使用@NoArgsConstructor注解的。三个注解都可以指定生成的构造方法的访问权限,同时,第二个注解还可以用@RequiredArgsConstructor(staticName=“methodName”)的形式生成一个指定名称的静态方法,返回一个调用相应的构造方法产生的对象,下面来看一个生动鲜活的例子:编译前的代码:@RequiredArgsConstructor(staticName = “sunsfan”)@AllArgsConstructor(access = AccessLevel.PROTECTED)@NoArgsConstructorpublic class Shape { private int x; @NonNull private double y; @NonNull private String name;}编译后的代码:public class Shape { private int x; private double y; private String name; public Shape() { } protected Shape(int x, double y, String name) { this.x = x; this.y = y; this.name = name; } public Shape(double y, String name) { this.y = y; this.name = name; } public static Shape sunsfan(double y, String name) { return new Shape(y, name); }}@Data/@Value@Data注解综合了@Getter/@Setter,@ToString,@EqualsAndHashCode和@RequiredArgsConstructor注解,其中@RequiredArgsConstructor使用了类中的带有@NonNull注解的或者final修饰的成员变量,它可以使用@Data(staticConstructor=“methodName”)来生成一个静态方法,返回一个调用相应的构造方法产生的对象。@Value注解和@Data类似,区别在于它会把所有成员变量默认定义为private final修饰,并且不会生成set方法。@SneakyThrows这个注解用在方法上,可以将方法中的代码用try-catch语句包裹起来,捕获异常并在catch中用Lombok.sneakyThrow(e)把异常抛出,可以使用@SneakyThrows(Exception.class)的形式指定抛出哪种异常,很简单的注解,直接看个例子:编译前的代码:public class SneakyThrows implements Runnable { @SneakyThrows(UnsupportedEncodingException.class) public String utf8ToString(byte[] bytes) { return new String(bytes, “UTF-8”); } @SneakyThrows public void run() { throw new Throwable(); }}编译后的代码:public class SneakyThrows implements Runnable { @SneakyThrows(UnsupportedEncodingException.class) public String utf8ToString(byte[] bytes) { try { return new String(bytes, “UTF-8”); } catch(UnsupportedEncodingException uee) { throw Lombok.sneakyThrow(uee); } } @SneakyThrows public void run() { try { throw new Throwable(); } catch(Throwable t) { throw Lombok.sneakyThrow(t); } }}@Synchronized这个注解用在类方法或者实例方法上,效果和synchronized关键字相同,区别在于锁对象不同,对于类方法和实例方法,synchronized关键字的锁对象分别是类的class对象和this对象,而@Synchronized的锁对象分别是私有静态final对象LOCK和私有final对象lock,当然,也可以自己指定锁对象,例子也很简单,往下看:编译前的代码:public class Synchronized { private final Object readLock = new Object(); @Synchronized public static void hello() { System.out.println(“world”); } @Synchronized public int answerToLife() { return 42; } @Synchronized(“readLock”) public void foo() { System.out.println(“bar”); }}编译后的代码:public class Synchronized { private static final Object $LOCK = new Object[0]; private final Object $lock = new Object[0]; private final Object readLock = new Object(); public static void hello() { synchronized($LOCK) { System.out.println(“world”); } } public int answerToLife() { synchronized($lock) { return 42; } } public void foo() { synchronized(readLock) { System.out.println(“bar”); } }}@Log这个注解用在类上,可以省去从日志工厂生成日志对象这一步,直接进行日志记录,具体注解根据日志工具的不同而不同,同时,可以在注解中使用topic来指定生成log对象时的类名。不同的日志注解总结如下(上面是注解,下面是编译后的代码):@CommonsLog==> private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);@JBossLog==> private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LogExample.class);@Log==> private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());@Log4j==> private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class);@Log4j2==> private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);@Slf4j==> private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);@XSlf4j==> private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);参考资料Spring Boot下的lombok安装以及使用简介lombok注解介绍 ...

March 16, 2019 · 3 min · jiezi

JavaConfig方式以及处理自动装配歧义性-spring基础学习

在XML配置和直接注解式配置之外还有一种有趣的选择方式-JavaConfig,java config是指基于java配置的spring。传统的Spring一般都是基本xml配置的,后来spring3.0新增了许多java config的注解,特别是spring boot,基本都是清一色的java config。下面用一段简单的程序来演示.使用IDEA创建一个Maven项目在pom.xml 中引用依赖 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.13.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.3.13.RELEASE</version> </dependency>项目结构图代码说明和实现Dao层和Service层不赘述了,简单实现一个add()方法完整代码已上传github:GitHubAppConfig配置类用@Configuration注解该类,等价 与XML中配置beans;用@Bean标注方法等价于XML中配置bean,被注解的类内部包含有一个或多个被@Bean注解的方法UserDaoNormal@Configurationpublic class AppConfig { @Bean public UserDao userDaoNormal(){ System.out.println(“创建UserDaoNormal对象”); return new UserDaoNormal(); @Bean public UserDao userDaoCache() { System.out.println(“创建UserDaoCache对象”); return new UserDaoCache(); } @Bean public UserService userServiceNormal(UserDao userDao){ System.out.println(“创建一个UserService对象”); return new UserServiceNormal(userDao); }}UserServiceTest测试类@RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境@ContextConfiguration Spring整合JUnit4测试时,使用注解引入多个配置文件多个配置文件:@ContextConfiguration(locations = { “classpath:/spring1.xml”, “classpath:/spring2.xml” })@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = AppConfig.class)public class UserServiceTest { @Autowired private UserService userService; @Test public void addTest(){ userService.add(); }}运行后却报异常出错!问题是出现了依赖注入的歧义性,UserDao不能够进行自动装配.简单来说,在spring容器中找到了一个以上的UserDao类型的对象,所以不知道到底要哪个,我在UserDao接口写了两个实现add()方法的类,有一个UserDaoNormal对象和一个UserDaoCache对象,所以无法正常进行依赖注入.解决的办法有几个:1.@primary注解在AppConfig配置文件里根据需要在两个对象方法上在其中一个添加 @primary注解,说明这个对象是依赖注入的首选bean.2.@Qualifier注解Qualifier的意思是合格者,通过这个标示,表明了哪个实现类才是我们所需要的,添加@Qualifier注解,需要注意的是@Qualifier的参数名称为我们之前定义@Qualifier注解的名称之一。代码如下@Configurationpublic class AppConfig { @Bean @Qualifier(“normal”) public UserDao userDaoNormal(){ System.out.println(“创建UserDaoNormal对象”); return new UserDaoNormal(); } @Bean @Qualifier(“cache”) public UserDao userDaoCache() { System.out.println(“创建UserDaoCache对象”); return new UserDaoCache(); } @Bean public UserService userServiceNormal(@Qualifier(“normal”) UserDao userDao){ System.out.println(“创建一个UserService对象”); return new UserServiceNormal(userDao); }}3.@Qualifier注解和bean id同样的,@Qualifier的参数名称为我们之前定义@bean注解的名称之一。代码如下: @Bean(“normal”) public UserDao userDaoNormal(){ System.out.println(“创建UserDaoNormal对象”); return new UserDaoNormal(); } @Bean(“cache”) public UserDao userDaoCache() { System.out.println(“创建UserDaoCache对象”); return new UserDaoCache(); } @Bean public UserService userServiceNormal(@Qualifier(“normal”) UserDao userDao){ System.out.println(“创建一个UserService对象”); return new UserServiceNormal(userDao);如果我们不给bean起一个约定的id,会有一个默认的id,实际上就是@bean所在的方法的方法名.@Qualifier的参数名称为@bean所在的方法的方法名的名称之一。代码如下:@Bean public UserDao userDaoNormal(){ System.out.println(“创建UserDaoNormal对象”); return new UserDaoNormal(); } @Bean public UserDao userDaoCache() { System.out.println(“创建UserDaoCache对象”); return new UserDaoCache(); } @Bean public UserService userServiceNormal(@Qualifier(“userDaoCache”) UserDao userDao){ System.out.println(“创建一个UserService对象”); return new UserServiceNormal(userDao); }4.运行后的两种结果 ...

March 16, 2019 · 1 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

paas平台搭建

背景公司项目中经常会用到缓存、消息队列等中间件,通常是直接配置在各个服务中,项目一多资源管理就比较混乱且资源配置比较繁琐。paas平台做资源集中管理,业务系统通过sdk集成服务,简化业务调用,方便资源管理。原理paas-service服务负责资源的管理分配zookeeper 做配置中心sdk 通过serviceId 鉴权,拿到zk地址,获得zk上的配置,初始化客户端服务开通流程a.生成serviceIdb.校验服务serviceId是否存在c.通过资源表获取redis-server信息d.处理redis-servere.添加zookeeper信息f.保存实例表记录SDK初始化流程a.通过serviceId,获得cacheclien对象;已有的话,直接返回,否则初始化b.认证serviceId,存在的话,返回zookeeper地址;否则结束c.从zookeeper上获取redis-server的信息,并watch该节点的变化d.初始化连接池jedispool,new cacheclient项目地址1、paas-service-web2、paas-sdk3、vue2-management-platform界面

March 15, 2019 · 1 min · jiezi

java垃圾收集算法

基础背景运行时数据区域虚拟机结构图程序计数器:每个线程独有一份,用作记录编译后的class文件行号虚拟机栈:以栈帧为单位存放局部变量.Native方法栈:和虚拟机栈类似,不过,一个本地方法是这样一个方法:该方法的实现由非java语言实现,比如C语言实现。很多其它的编程语言都有这一机制,比如在C++中,你可以告知C++编译器去调用一个C语言编写的方法方法区:运行时常量池,存放编译器的字面量和符号引用,也可以在运行时动态加入.java堆:存放对象的实例,是垃圾回收的主战场,创建一个对象比如执行 new MyClass();去常量池中寻找,查看类是否被加载.如果没加载,则加载class.在java堆中分配内存空间,方式有以下两种:指针碰撞:把指针向空闲对象移动与对象占用内存大小相等的距离,使用的收集器有Serial、ParNes等空闲列表:虚拟机维护一个列表,记录可用的内存块,分配给对象列表中一块足够大的内存空间,使用的收集器有CMS等.如何分配内存,由垃圾回收器决定.内存的具体分配过程中有同步和预留空白区的方式内存分配好后,再执行init()方法,初始化实例.对象头对象头主要记录对象的hashcode,GC标记,元数据地址,以及关于对象锁的使用,年龄代,偏向线程等。hash用于快速寻找对象对象头大小32bit/64bit,由虚拟机决定实例数据区的数据类型,按照相似放在一起.对象中的访问定位方式句柄池句柄池从堆中划分由实例地址和类型数据地址构成指针可直接通过指针访问到实例对象优劣句柄的使用,方便了实例位置的改变,可以不改变引用,但是访问速度相对于指针低一些.JVM垃圾回收判断可否回收的算法1.引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。可达性分析算法:通过一系列的名为GC Root的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。实例的位置:java虚拟机栈(栈帧中的本地变量表)中的引用的对象。方法区中的类静态属性引用的对象。方法区中的常量引用的对象。本地方法栈中JNI本地方法的引用对象。不可达对象到死亡还需要两次标记,第一次,标记后进入F-Queue队列,第二次标记时只有finalize()中有拯救自己的方法的实例才能自救成功,比如将自己应用给其它变量.垃圾回收算法方法区的回收常量池的回收没有被引用,即可被回收class对象回收所有实例都被回收所有classLoader都被回收java.lang.class对象没有被任何地方引用,即无法在任何地方使用反射访问类.最终是否被回收,还得看JVM参数配置java堆回收算法标记清除算法: 先标记判定,再一次性清除.产生了大量碎片,且效率低下复制算法: 把可用内存划分为两块,一块用完后,就将活下来的实例放到另一块内存区.优缺点:没有了碎片化问题,但内存大小减少了一半标记整理算法: 在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记整理算法效率会大大提高。分代收集算法: 根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。现在的Java虚拟机就联合使用了分代复制、标记-清除和标记-整理算法.java虚拟机垃圾收集器关注的内存结构如下:新生代研究表明,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from和Survivor to三部分,其占新生代内存容量默认比例分别为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,只有Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。年老代年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。堆分配和回收策略分配优先在Eden上分配,空间不足,虚拟机发起minor GC.大对象直接进入老年代,防止折磨新生代空间.[参数设置 -XX:PretrnureSizeThreshold=[字节数]]长大后的对象进入老年代,在survivor中熬过一次,就长一岁,15岁时就进入老年代[阈值设置 -XX:MaxTenuringThreshold=[岁数]]相同年龄的对象,若大于或等于空间的一半,也直接进入老年代.附:JVM参数整理参数调优建议永久代:-XX:PermSize20M -XX:MaxPermSize20M堆大小: -Xms20M -Xmx20M新生代: -Xmn10MEden与survior比率: -XX:SurvivorRation=8让大对象直接进入老年代: -XX:PretrnureSizeThreshold=1B年龄阈值:-XX:MaxTenuringThreshold=15日志命令:参考连接

March 15, 2019 · 1 min · jiezi

shiro入门笔记

关于 Apache Shiro 概念基本都粘自官网 http://shiro.apache.org/Shiro 简介Apache Shiro是一个功能强大且灵活的开源安全框架,可以清晰地处理身份验证,授权,企业会话管理和加密。以下是Apache Shiro可以做的一些事情:验证用户以验证其身份为用户执行访问控制在任何环境中使用Session API,即使没有Web容器或EJB容器也是如此。……功能简介Authentication:身份认证/登录,验证用户是不是拥有相应的身份;Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的;Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;Web Support:Web 支持,可以非常容易的集成到Web 环境;Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;Testing:提供测试支持;Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了Shiro 详细的架构可以参考官方文档:http://shiro.apache.org/archi…Shiro web工程搭建1.Maven 架包依赖缓存架包先用 ehcache<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.3.2</version></dependency><dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> <version>2.10.6</version></dependency>2.ehcache 缓存xml文件配置<?xml version=“1.0” encoding=“UTF-8”?><ehcache> <!– mac 电脑, 跟 win 设置路径有点不一样 示例: path=“d:/ehcache/” –> <diskStore path="${user.home}/Downloads/ehcache" /> <!– 默认缓存配置 没有特别指定就用这个配置 –> <defaultCache maxElementsInMemory=“10000” eternal=“false” timeToIdleSeconds=“3600” timeToLiveSeconds=“3600” overflowToDisk=“true” maxElementsOnDisk=“10000000” diskPersistent=“false” memoryStoreEvictionPolicy=“LRU” diskExpiryThreadIntervalSeconds=“120” /></ehcache>3.web.xml中shiro拦截器配置<!– 1. 配置 shiro 拦截器 –><filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param></filter><filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/</url-pattern></filter-mapping>4.spring文件配置<!– 2. 配置shiro的核心组件 SecurityManager –><bean id=“securityManager” class=“org.apache.shiro.web.mgt.DefaultWebSecurityManager”> <!– 配置缓存 –> <property name=“cacheManager” ref=“cacheManager” /> <property name=“realm” ref=“realm” /></bean><bean id=“cacheManager” class=“org.apache.shiro.cache.ehcache.EhCacheManager”> <property name=“cacheManagerConfigFile” value=“classpath:ehcache.xml”></property></bean><bean id=“realm” class=“com.ogemray.shiro.MyShiroRealm”> <property name=“credentialsMatcher”> <bean class=“com.ogemray.shiro.MyCredentialsMatcher”></bean> </property></bean><!– 3. 配置shiroFilter,配置shiro的一些基本规则信息,id必须和web.xml中配置的拦截器名字一样(DelegatingFilterProxy 通过名字去spring容器中找注入的拦截器) –><bean id=“shiroFilter” class=“org.apache.shiro.spring.web.ShiroFilterFactoryBean”> <property name=“securityManager” ref=“securityManager”/> <property name=“loginUrl” value="/login.html"/> <!– loginUrl 表示登录页面地址 –> <property name=“successUrl” value="/admin/main.html"/> <!– successUrl 表示登录成功后跳转的页面 –> <property name=“unauthorizedUrl” value="/unauthorized.html"></property> <!– 配置没有授权跳转页面 –> <property name=“filterChainDefinitions”> <value> <!–/logout.action=logout –> <!– logout 表示登出, 清空session, 这里不需要了, 因为已经在登出对象的方法里手动清空了 [ SecurityUtils.getSubject().logout() ] –> /admin/userlist=roles[user] /admin/adduser*=roles[“user,admin”] <!– 表示拥有 user 角色 并且 拥有 admin 角色 –> /admin/editRPRelation*=roles[admin],perms[user:insert,user:update,user:select,user:delete] /admin/editURRelation*=perms[user:select] /admin/=authc /=anon </value> </property></bean>5.自定义realmpublic class MyShiroRealm extends AuthorizingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("********************* 进行登录认证 "); String username = (String) authenticationToken.getPrincipal(); //获取提交的用户名 User user = userRepository.findByUsername(username); if (user == null) throw new UnknownAccountException(“用户不存在, 请先注册然后再来登录”); if (user.getState() == 1) throw new LockedAccountException(“该用户已经被管理员禁用, 请换个账号登录”); //接下来进行密码的比对 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); return info; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println(" 进行授权认证 *********************"); User user = (User) principalCollection.asList().get(0); Set<String> roles = new HashSet<>(); Set<String> permissions = new HashSet<>(); user.getRoles().forEach(role -> { roles.add(role.getRoleName()); role.getPermissions().forEach(permission -> { permissions.add(permission.getPermissionName()); }); }); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setRoles(roles); info.setStringPermissions(permissions); return info; } @Autowired private UserRepository userRepository;}6.自定义密码匹对方案前端密码加密规则:ciphertext_pwd = AES.encrypt(MD5(password))后端解密密码规则:md5_password = AES.desEncrypt(ciphertext_pwd)后端匹对密码规则:(md5_password + 用户名做盐值) 进行 1024 次 MD5 转换,然后与数据库取出密码做比对public class MyCredentialsMatcher implements CredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String tokenCredentials = Arrays.toString((char[]) token.getCredentials()); //前端传过来的密码 String accountCredentials = (String) info.getCredentials(); //数据库查询到的密码 //首先对前端传过来的密码进行AES解密 -> 清空 session 里面的key Session session = SecurityUtils.getSubject().getSession(); String key = (String) session.getAttribute(“AESKey”); try { tokenCredentials = AesEncryptUtil.desEncrypt(tokenCredentials, key, key); } catch (Exception e) { throw new IncorrectCredentialsException(“可能受到重放攻击, AES 解密失败”); } session.removeAttribute(“AESKey”); //加密方式 待加密数据 加密盐值 加密次数 SimpleHash simpleHash = new SimpleHash(“MD5”, tokenCredentials, token.getPrincipal(), 1024); return simpleHash.toString().equals(accountCredentials); }}7.登录和注册接口的调用@Service(“userService”)public class UserServiceImpl implements UserService { @Override public void registerUser(User user) { if (userRepository.existsByUsername(user.getUsername())) { throw new RuntimeException(“用户名已经被注册, 请换个用户名”); } user.setState((byte) 0); //密码进行加密 SimpleHash simpleHash = new SimpleHash(“MD5”, user.getPassword(), user.getUsername(), 1024); user.setPassword(simpleHash.toString()); userRepository.save(user); } @Override public void login(User user) { Subject currentUser = SecurityUtils.getSubject(); if (currentUser.isAuthenticated() == false) { //没有登录过需要进行登录验证 UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword(), false); currentUser.login(token); } } @Autowired private UserRepository userRepository;} ...

March 14, 2019 · 2 min · jiezi

spring boot学习(3): SpringApplication 事件监听

spring application listener在 spring 框架中,有多种事件, 这些时间会在不同的运行时刻发布,来通知监听者。本文仅仅介绍 SpringApplicationEvent 的事件的监听。事件类型EventType发布时间ApplicationContextInitializedEvent在 SpringApplication正在启动, ApplicationContext 已经准备好了,ApplicationContextInitializers 被调用, bean definitions 被加载之前ApplicationStartingEvent在一次启动之前发布ApplicationEnvironmentPreparedEvent在 Environment 准备好之后,会有 context 去使用这一 Environment, 会在 context 创建之前发出ApplicationPreparedEvent会在 bean definitions 加载之后,refresh 之前发布ApplicationStartedEventcontext 更新之后,任何应用或命令行启动调用之前ApplicationReadyEvent任何应用或命令行启动调用之后发布,说明应用已经可以被请求了ApplicationFailedEvent启动发生有异常时发步如何监听监听器需要使用 org.springframework.context.ApplicationListener 这个接口的实例, 其声明如下:@FunctionalInterfacepublic interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /** * Handle an application event. * @param event the event to respond to */ void onApplicationEvent(E event);}需要使用 SpringApplication.addListeners(…) 或 SpringApplicationBuilder.listeners(…) 来添加监听器。也可以在 META-INF/spring.factories 文件中配置:org.springframework.context.ApplicationListener=com.example.project.MyListener。例子:public class StartingEventListener implements ApplicationListener<ApplicationStartingEvent> { @Override public void onApplicationEvent(ApplicationStartingEvent applicationStartingEvent) { System.out.println(“called own starting listener”); System.out.println(applicationStartingEvent.getClass()); }}@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args){ SpringApplication application = new SpringApplication(DemoApplication.class); application.addListeners(new StartingEventListener()); application.run(args); }}终端运行 jar 包:$ java -jar build/libs/springlisteners-0.0.1-SNAPSHOT.jarcalled own starting listenerclass org.springframework.boot.context.event.ApplicationStartingEvent . ____ _ __ _ _ /\ / ’ __ _ () __ __ _ \ \ \ ( ( )__ | ‘_ | ‘| | ‘ / ` | \ \ \ \ \/ )| |)| | | | | || (| | ) ) ) ) ’ || .__|| ||| |_, | / / / / =========||==============|/=//// :: Spring Boot :: (v2.1.3.RELEASE) ...

March 14, 2019 · 1 min · jiezi

SpringCloud基础篇AOP之拦截优先级详解

相关文章可以查看: http://spring.hhui.top190301-SpringBoot基础篇AOP之基本使用姿势小结190302-SpringBoot基础篇AOP之高级使用技能前面两篇分别介绍了AOP的基本使用姿势和一些高级特性,当时还遗留了一个问题没有说明,即不同的advice,拦截同一个目标方法时,优先级是怎样的,本篇博文将进行详细分析同一个切面中,不同类型的advice的优先级同一个切面中,同一种类型的advice优先级不同切面中,同一类型的advice优先级不同切面中,不同类型的advice优先级<!– more –>I. 统一切面,不同类型ddvice优先级在不分析源码的前提下,也只能通过实际的case来看优先级问题了,我们现在设计一下使用实例,通过输出结果来看对应的优先级1. case设计首先创建被拦截的bean: com.git.hui.boot.aop.order.InnerDemoBean@Componentpublic class InnerDemoBean { public String print() { try { System.out.println(“in innerDemoBean start!”); String rans = System.currentTimeMillis() + “|” + UUID.randomUUID(); System.out.println(rans); return rans; } finally { System.out.println(“in innerDemoBean over!”); } }}接下来写一个切面,里面定义我们常见的各种advice对于aop的使用,有疑问的可以参考: 190301-SpringBoot基础篇AOP之基本使用姿势小结@Component@Aspectpublic class OrderAspect { @Pointcut(“execution(public * com.git.hui.boot.aop.order..())”) public void point() { } @Before(value = “point()”) public void doBefore(JoinPoint joinPoint) { System.out.println(“do before!”); } @After(value = “point()”) public void doAfter(JoinPoint joinPoint) { System.out.println(“do after!”); } @AfterReturning(value = “point()”, returning = “ans”) public void doAfterReturning(JoinPoint joinPoint, String ans) { System.out.println(“do after return: " + ans); } @Around(“point()”) public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { try { System.out.println(“do in around before”); return joinPoint.proceed(); } finally { System.out.println(“do in around over!”); } }}2. 测试使用SpringBoot的项目进行测试aop,使用还是比较简单的@SpringBootApplicationpublic class Application { private InnerDemoBean innerDemoBean; public Application(InnerDemoBean innerDemoBean) { this.innerDemoBean = innerDemoBean; this.innerDemoBean(); } private void innerDemoBean() { System.out.println(“result: " + innerDemoBean.print()); } public static void main(String[] args) { SpringApplication.run(Application.class); }}看下上面执行的输出结果do in around beforedo before!in innerDemoBean start!1552219604035|e9a31f44-6a31-4485-806a-834361842ce1in innerDemoBean over!do in around over!do after!do after return: 1552219604035|e9a31f44-6a31-4485-806a-834361842ce1result: 1552219604035|e9a31f44-6a31-4485-806a-834361842ce1从输出结果进行反推,我们可以知道统一切面中,advice执行的先后顺序如下II. 同一切面,同一类型切面正常来讲,拦截一个方法时,统一类型的切面逻辑都会写在一起,那这个case有什么分析的必要呢?在我们实际的使用中,同一类型的advice拦截同一个方法的可能性还是很高的,why? 因为多个advice有自己定义的拦截规则,它们之间并不相同,但可能存在交集,比如我们在上面的切面中,再加一个拦截注解的before advice1. case设计依然是上面的InnerDemoBean,方法上加一个自定义注解@AnoDotpublic String print() { try { System.out.println(“in innerDemoBean start!”); String rans = System.currentTimeMillis() + “|” + UUID.randomUUID(); System.out.println(rans); return rans; } finally { System.out.println(“in innerDemoBean over!”); }}然后加一个拦截注解的advice@Before("@annotation(AnoDot)")public void doAnoBefore(JoinPoint joinPoint) { System.out.println(“dp AnoBefore”);}2. 测试再次执行前面的case,然后看下输出结果如下In NetAspect doAround before!do in around beforedp AnoBeforedo before!in innerDemoBean start!1552221765322|d92b6d37-0025-43c0-adcc-c4aa7ba639e0in innerDemoBean over!do in around over!do after!do after return: 1552221765322|d92b6d37-0025-43c0-adcc-c4aa7ba639e0In NetAspect doAround over! ans: 1552221765322|d92b6d37-0025-43c0-adcc-c4aa7ba639e0result: 1552221765322|d92b6d37-0025-43c0-adcc-c4aa7ba639e0我们主要看下两个before,发现 AnoBefore 在前面; 因此这里的一个猜测,顺序就是根据方法命名的顺序来的,比如我们再加一个 doXBefore,然后我们预估输出结果应该是do AnoBefore > doBefore > doXBefore额外添加一个@Before("@annotation(AnoDot)")public void doXBefore(JoinPoint joinPoint) { System.out.println(“dp XBefore”);}接着就是输出结果如下,和我们预期一致3. Order注解尝试我们知道有个Order注解可以来定义一些优先级,那么把这个注解放在advice方法上,有效么?实际尝试一下@Order(1)@Before(value = “point()")public void doBefore(JoinPoint joinPoint) { System.out.println(“do before!”);}@Order(2)@Before("@annotation(AnoDot)")public void doAnoBefore(JoinPoint joinPoint) { System.out.println(“dp AnoBefore”);}@Order(3)@Before("@annotation(AnoDot)")public void doXBefore(JoinPoint joinPoint) { System.out.println(“dp XBefore”);}如果注解有效,我们预期输出结果如下do Before > do AnoBefore > do XBefore然后再次执行,看下输出结果是否和我们预期一样4. 小结同一个切面中,相同的类型的advice,优先级是根据方法命名来的,加@Order注解是没有什么鸟用的,目前也没有搜索到可以调整优先级的方式III. 不同切面,相同类型的advice如果说上面这种case不太好理解为啥会出现的话,那么这个可能就容易理解多了;毕竟一个切面完成一件事情,出现相同的advice就比较常见了;比如spring mvc中,我们通常会实现的几个切面一个before advice的切面,实现输出请求日志一个before advice的切面,实现安全校验(这种其实更常见的是放在filter/intercept中)1. case设计现在就需要再加一个切面,依然以before advice作为case@Aspect@Componentpublic class AnotherOrderAspect { @Before("@annotation(AnoDot)”) public void doBefore() { System.out.println(“in AnotherOrderAspect before!”); }}2. 测试接下来看测试输出结果如下图发现了一个有意思的事情了,AnotherOrderAspect切面的输出,完全在OrderAspect切面中所有的advice之前,接着我们再次尝试使用@Order注解来试试,看下会怎样@Order(0)@Component@Aspectpublic class OrderAspect {}@Aspect@Order(10)@Componentpublic class AnotherOrderAspect {}如果顺序有关,我们预期的输出结果应该是do AnoBefore > do Before > doXBefore > do AnotherOrderAspect before!实际测试输出如下,和我们预期一致3. 小结从上面的测试来看,不同的切面,默认顺序实际上是根据切面的命令来的;A切面中的advice会优先B切面中同类型的advice我们可以通过 Order 注解来解决不同切面的优先级问题,依然是值越小,优先级越高IV. 不同切面,不同advice顺序其实前面的case已经可以说明这个问题了,现在稍稍丰富一下AnotherOrderAspect,看下结果1. case设计@Aspect@Order(10)@Componentpublic class AnotherOrderAspect { @Before("@annotation(AnoDot)”) public void doBefore() { System.out.println(“in AnotherOrderAspect before!”); } @After("@annotation(AnoDot)”) public void doAfter(JoinPoint joinPoint) { System.out.println(“do AnotherOrderAspect after!”); } @AfterReturning(value = “@annotation(AnoDot)”, returning = “ans”) public void doAfterReturning(JoinPoint joinPoint, String ans) { System.out.println(“do AnotherOrderAspect after return: " + ans); } @Around("@annotation(AnoDot)”) public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { try { System.out.println(“do AnotherOrderAspect in around before”); return joinPoint.proceed(); } finally { System.out.println(“do AnotherOrderAspect in around over!”); } }}2. 测试看下执行后的输出结果假设A切面优先级高于B切面,那么我们执行先后顺序如下V. 小结本篇内容有点多,针对前面的测试以及结果分析,给出一个小结,方便直接获取最终的答案1. 不同advice之间的优先级顺序around 方法执行前代码 > before > 方法执行 > around方法执行后代码 > after > afterReturning/@AfterThrowing2. 统一切面中相同advice统一切面中,同类型的advice的优先级根据方法名决定,暂未找到可以控制优先级的使用方式3. 不同切面优先级不同切面优先级,推荐使用 @Order注解来指定,数字越低,优先级越高4. 不同切面advice执行顺序优先级高的切面中的advice执行顺序会呈现包围优先级低的advice的情况,更直观的先后顺序,推荐看第四节的顺序图,更加清晰明了VI. 其他0. 项目工程:https://github.com/liuyueyi/spring-boot-demomodule: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/010-aop1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

March 12, 2019 · 2 min · jiezi

时速云中标华数传媒基于容器的自动化运维平台项目

继时速云中标华数数字电视传媒集团有限公司(简称华数传媒)容器云平台项目之后,2019年2月,时速云再次中标华数传媒基于容器的自动化运维平台项目。中国新媒体产业发展第一方阵华数传媒是大型国有文化传媒产业集团,位居中国新媒体产业发展第一方阵。华数传媒紧抓国家推进传统媒体与新兴媒体融合发展的战略契机,以“新网络+应用”、“新媒体+内容”、“大数据+开发”三大发展战略为指导,以打造“智慧华数、融合媒体”为目标,积极构建新网络、新媒体、云计算和大数据、原创内容以及智慧化等产业板块,全面加快向智慧广电综合运营商和数字经济发展主体转型。目前,华数集团拥有有线网络业务、手机电视与互联网电视等全国新媒体业务以及宽带网络业务方面的运营牌照,覆盖海量传统媒体和新媒体用户,包括3000万有线电视家庭用户、1亿互联网电视覆盖用户、5600万华数手机电视用户。华数传媒客户群体庞大,拥有大量的基础设施,存在管理难度大,开发和集成速度慢,难以维护和发展,升级服务时启动慢,无感知升级困难,升级时环境易被破坏等问题。为减少运维人员、开发人员、测试人员工作量,减少反复冗杂的开发、测试、升级流程,华数传媒需要基于容器技术,打造新一代自动化运维平台,并于2019年年初对该项目进行公开招标。华数传媒基于容器的自动化运维平台在功能及性能方面需满足以下要求:功能要求:1、集成开发环境,实行自动化测试,灰度发布,正式发布2、减少升级、配置文件修改等操作时间,提升版本发布速率3、对于应用版本镜像的管理4、实现故障自动化隔离、自动化恢复5、服务器压力阈值自动化扩容性能要求:1、整体服务性能提升10-50%2、安装完成所需插件(Docker)后,服务器整体资源占用率小于5%再次中标,时速云在华数传媒基于容器的自动化运维平台项目招标中脱颖而出时速云是国内领先的容器云计算服务提供商,业务涵盖容器 PaaS 平台、DevOps、微服务治理、AIOps 等领域。拥有金融、能源、运营商、制造、广电、汽车等领域的诸多大型企业及世界500强客户。2018年,时速云曾中标华数传媒容器云平台项目,对基于 Docker 容器技术的 PaaS 平台进行开发部署,以便承载华数互动电视及新媒体业务系统,同时提供快速弹性伸缩、应用管理、监控运行等能力。此次,时速云再次凭借过硬的技术实力、高品质的服务能力以及丰富的实践经验,在华数传媒基于容器的自动化运维平台项目的招标中脱颖而出。通过容器云计算助力企业数字化转型是时速云的核心使命。目前,时速云已经为国家电网、新奥集团、戴姆勒奔驰、铂涛等众多知名企业成功交付了容器云 PaaS 平台及相关产品。再次中标华数传媒基于容器的自动化运维平台项目,是客户对时速云技术、产品及服务的又一次认可和肯定。此后,时速云将不断加大研发投入,把握行业发展机遇,为企业数字化、智能化转型提供更高品质的产品和服务,为企业创新、可持续发展保驾护航。

March 12, 2019 · 1 min · jiezi

几个数据持久化框架Hibernate、JPA、Mybatis、JOOQ和JDBC Template的比较

因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框架,对于复杂业务系统,最终的结论是,JOOQ是总体上最好的,可惜不是完全免费,最终选择JDBC Template。Hibernate和Mybatis是使用最多的两个主流框架,而JOOQ、Ebean等小众框架则知道的人不多,但也有很多独特的优点;而JPA则是一组Java持久层Api的规范,Spring Data JPA是JPA Repository的实现,本来和Hibernate、Mybatis、JOOQ之类的框架不在同一个层次上,但引入Spring Data JPA之类框架之后,我们会直接使用JPA的API查询更新数据库,就像我们使用Mybatis一样,所以这里也把JPA和其他框架放在一起进行比较。同样,JDBC和其他框架也在同一层次,位于所有持久框架的底层,但我们有时候也会直接在项目中使用JDBC,而Spring JDBC Template部分消除了使用JDBC的繁琐细节,降低了使用成本,使得我们更加愿意在项目中直接使用JDBC。一、SQL封装和性能在使用Hibernate的时候,我们查询的是POJO实体类,而不再是数据库的表,例如hql语句 select count(*) from User,里面的User是一个Java类,而不是数据库表User。这符合ORM最初的理想,ORM认为Java程序员使用OO的思维方式,和关系数据库的思维方式差距巨大,为了填补对象和关系思维方式的鸿沟,必须做一个对象到关系的映射,然后在Java的对象世界中,程序员可以使用纯的对象的思维方式,查询POJO对象,查询条件是对象属性,不再需要有任何表、字段等关系的概念,这样java程序员就更容易做持久层的操作。JPA可以视为Hibernate的儿子,也继承了这个思路,把SQL彻底封装起来,让Java程序员看不到关系的概念,用纯的面向对象思想,重新创造一个新的查询语言代替sql,比如hql,还有JPQL等。支持JPA的框架,例如Ebean都属于这种类型的框架。但封装SQL,使用另一种纯的面向对象查询语言代替sql,真的能够让程序员更容易实现持久层操作吗?MyBatis的流行证明了事实并非如此,至少在大多数情况下,使用hql并不比使用sql简单。首先,从很多角度上看,hql/JPQL等语言更加复杂和难以理解;其次就是性能上明显降低,速度更慢,内存占用巨大,而且还不好优化。最为恼火的是,当关系的概念被替换为对象的概念之后,查询语言的灵活性变得很差,表达能力也比sql弱很多。写查询语句的时候受到各种各样的限制,一个典型的例子就是多表关联查询。不管是hibernate还是jpa,表之间的连接查询,被映射为实体类之间的关联关系,这样,如果两个实体类之间没有(实现)关联关系,你就不能把两个实体(或者表)join起来查询。这是很恼火的事情,因为我们很多时候并不需要显式定义两个实体类之间的关联关系就可以实现业务逻辑,如果使用hql,只是为了join我们就必须在两个实体类之间添加代码,而且还不能逆向工程,如果表里面没有定义外键约束的话,逆向工程会把我们添加的关联代码抹掉。MyBatis则是另外一种类型的持久化框架,它没有封装SQL也没有创建一种新的面相对象的查询语言,而是直接使用SQL作为查询语言,只是把结果填入POJO对象而已。使用sql并不比hql和JPQL困难,查询速度快,可以灵活使用任意复杂的查询只要数据库支持。从SQL封装角度上看,MyBatis比Hibernate和JPA成功,SQL本不该被封装和隐藏,让Java程序员使用SQL既不麻烦也更容易学习和上手,这应该是MyBatis流行起来的重要原因。轻量级持久层框架JOOQ也和MyBatis一样,直接使用SQL作为查询语言,比起MyBatis,JOOQ虽然知名度要低得多,但JOOQ不但和MyBatis一样可以利用SQL的灵活性和高效率,通过逆向工程,JOOQ还可以用Java代码来编写SQL语句,利用IDE的代码自动补全功能,自动提示表名和字段名,减少程序员记忆负担,还可以在元数据发生变化时发生编译错误,提示程序员修改相应的SQL语句。Ebean作为一种基于JPA的框架,它也使用JPQL语言进行查询,多数情况下会让人很恼火。但据说Ebean不排斥SQL,可以直接用SQL查询,也可以用类似JOOQ的DSL方式在代码中构造SQL语句(还是JPQL语句?),但没用过Ebean,所以具体细节不清楚。JDBC Template就不用说了,它根本没做ORM,当然是纯SQL查询。利用Spring框架,可以把JDBC Template和JPA结合起来使用,在JPA不好查询的地方,或者效率低不好优化的地方使用JDBC,缓解了Hibernate/JPA封装SQL造成的麻烦,但我仍没看到任何封装SQL的必要性,除了给程序员带来一大堆麻烦和学习负担之外,没有太明显的好处。二、DSL和变化适应性为了实现复杂的业务逻辑,不论是用SQL还是hql或者JPQL,我们都不得不写很多简单的或者复杂的查询语句,ORM无法减少这部分工作,最多是用另一种面向对象风格的语言去表达查询需求,如前所述,用面向对象风格的语言不见得比SQL更容易。通常业务系统中会有很多表,每个表都有很多字段,即便是编写最简单的查询语句也不是一件容易的事情,需要记住数据库中有哪些表,有哪些字段,记住有哪些函数等。写查询语句很多时候成为一件头疼的事情。QueryDSL、JOOQ、Ebean甚至MyBatis和JPA都设计一些特性,帮助开发人员编写查询语句,有人称之为“DSL风格数据库编程”。最早实现这类功能的可能是QueryDSL,把数据库的表结构逆向工程为java的类,然后可以让java程序员能够用java的语法构造出一个复杂的查询语句,利用IDE的代码自动补全功能,可以自动提示表名、字段名、查询语句的关键字等,很成功的简化了查询语句的编写,免除了程序员记忆各种名字、函数和关键字的负担。QueryDSL有很多版本,但用得多的是QueryDSL JPA,可以帮助开发人员编写JPQL语句,如前所述,JPQL语句有很多局限不如SQL灵活高效。后来的JOOQ和Ebean,基本上继承了QueryDSL的思路,Ebean基本上还是JPA风格的ORM框架,虽然也支持SQL,但不清楚其DSL特性是否支持SQL语句编写,在官网上看到的例子都是用于构造JPQL语句。这里面最成功的应该是JOOQ,和QueryDSL不同,JOOQ的DSL编程是帮助开发人员编写SQL语句,抛弃累赘的ORM概念,JOOQ这个功能非常轻小,非常容易学习和使用,同时性能也非常好,不像QueryDSL和Ebean,需要了解复杂的JPA概念和各种奇异的限制,JOOQ编写的就是普通的SQL语句,只是把查询结果填充到实体类中(严格说JOOQ没有实体类,只是自动生成的Record对象),JOOQ甚至不一定要把结果转换为实体类,可以让开发人员按照字段取得结果的值,相对于JDBC,JOOQ会把结果值转换为合适的Java类型,用起来比JDBC更简单。传统主流的框架对DSL风格支持得很少,Hibernate里面基本上没有看到有这方面的特性。MyBatis提供了"SQL语句构建器"来帮助开发人员构造SQL语句,但和QueryDSL/JOOQ/Ebean差很多,不能提示表名和字段名,语法也显得累赘不像SQL。JPA给人的印象是复杂难懂,它的MetaModel Api继承了特点,MetaModel API+Criteria API,再配合Hibernate JPA 2 Metamodel Generator,让人有点QueryDSL JPA的感觉,只是绕了一个大大的弯,叠加了好几层技术,最后勉强实现了QueryDSL JPA的简单易懂的功能。很多人不推荐JPA+QueryDSL的用法,而是推荐JPA MetaModel API+Criteria API+Hibernate JPA 2 Metamodel Generator的用法,让人很难理解,也许是因为这个方案是纯的标准的JPA方案。数据库DSL编程的另一个主要卖点是变化适应性强,数据库表结构在开发过程中通常会频繁发生变化,传统的非DSL编程,字段名只是一个字符串,如果字段名或者类型改变之后,查询语句没有相应修改,编译不会出错,也容易被开发人员忽略,是bug的一个主要来源。DSL编程里面,字段被逆向工程为一个java类的属性,数据库结构改变之后,作为java代码一部分的查询语句会发生编译错误,提示开发人员进行修改,可以减少大量bug,减轻测试的负担,提高软件的可靠性和质量。三、跨数据库移植Hibernate和JPA使用hql和JPQL这类数据库无关的中间语言描述查询,可以在不同数据库中无缝移植,移植到一个SQL有巨大差别的数据库通常不需要修改代码或者只需要修改很少的代码。Ebean如果不使用原生SQL,而是使用JPA的方式开发,也能在不同数据库中平滑的移植。MyBatis和JOOQ直接使用SQL,跨数据库移植时都难免要修改SQL语句。这方面MyBatis比较差,只有一个动态SQL提供的特性,对于不同的数据库编写不同的sql语句。JOOQ虽然无法像Hibernate和JPA那样无缝移植,但比MyBatis好很多。JOOQ的DSL很大一部分是通用的,例如分页查询中,Mysql的limit/offset关键字是很方便的描述方式,但Oracle和SQLServer的SQL不支持,如果我们用JOOQ的DSL的limit和offset方法构造SQL语句,不修改移植到不支持limit/offset的Oracle和SQLServer上,我们会发现这些语句还能正常使用,因为JOOQ会把limit/offset转换成等价的目标数据库的SQL语句。JOOQ根据目标数据库转换SQL语句的特性,使得在不同数据库之间移植的时候,只需要修改很少的代码,明显优于MyBatis。JDBC Template应该最差,只能尽量使用标准sql语句来减少移植工作量。四、安全性一般来说,拼接查询语句都会有安全隐患,容易被sql注入攻击。不论是jdbc,还是hql/JPQL,只要使用拼接的查询语句都是不安全的。对于JDBC来说,使用参数化的sql语句代替拼接,可以解决问题。而JPA则应该使用Criteria API解决这个问题。对于JOOQ之类的DSL风格框架,最终会被render为参数化的sql,天生免疫sql注入攻击。Ebean也支持DSL方式编程,也同样免疫sql注入攻击。这是因为DSL风格编程参数化查询比拼接字符串查询更简单,没人会拼接字符串。而jdbc/hql/JPQL拼接字符串有时候比参数化查询更简单,特别是jdbc,很多人会偷懒使用不安全的方式。五、JOOQ的失败之处可能大部分人会不同意,虽然Hibernate、JPA仍然大行其道,是最主流的持久化框架,但其实这种封装SQL的纯正ORM已经过时,效益低于使用它们的代价,应该淘汰了。MyBatis虽然有很多优点,但它的优点JOOQ基本上都有,而且多数还更好。MyBatis最大的缺点是难以避免写xml文件,xml文件编写困难,容易出错,还不容易查找错误。相对于JOOQ,MyBatis在多数情况下没有任何优势。Ebean同时具有很多不同框架的优点,但它是基于JPA的,难免有JPA的各种限制,这是致命的缺点。JOOQ这个极端轻量级的框架技术上是最完美的,突然有一天几个Web系统同时崩了,最后发现是JOOQ试用期过期了,这是JOOQ的失败之处,它不是完全免费的,只是对MySql之类的开源数据库免费。最终,我决定选择JDBC Template。

March 12, 2019 · 1 min · jiezi

Spring源码一(容器的基本实现3)

前言:继续前一章,接下来解析Bean标签的的属性信息。1. 解析当前bean标签的内容当我们创建了bean信息的承载实例之后, 便可以进行bean信息的各种属性的解析了, 首先我们进入parseBeanDefinitionAttributes方法,parseBeanDefinitionAttributes方法是对element所有元素属性进行解析:/** * Apply the attributes of the given bean element to the given bean * definition. * @param ele bean declaration element * @param beanName bean name * @param containingBean containing bean definition * @return a bean definition initialized according to the bean element attributes */public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, @Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) { if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) { error(“Old 1.x ‘singleton’ attribute in use - upgrade to ‘scope’ declaration”, ele); } else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) { bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE)); } else if (containingBean != null) { // Take default from containing bean in case of an inner bean definition. bd.setScope(containingBean.getScope()); } if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) { bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE))); } String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE); if (DEFAULT_VALUE.equals(lazyInit)) { lazyInit = this.defaults.getLazyInit(); } bd.setLazyInit(TRUE_VALUE.equals(lazyInit)); String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE); bd.setAutowireMode(getAutowireMode(autowire)); if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) { String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE); bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS)); } String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE); if ("".equals(autowireCandidate) || DEFAULT_VALUE.equals(autowireCandidate)) { String candidatePattern = this.defaults.getAutowireCandidates(); if (candidatePattern != null) { String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern); bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName)); } } else { bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate)); } if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) { bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE))); } if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) { String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE); bd.setInitMethodName(initMethodName); } else if (this.defaults.getInitMethod() != null) { bd.setInitMethodName(this.defaults.getInitMethod()); bd.setEnforceInitMethod(false); } if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) { String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE); bd.setDestroyMethodName(destroyMethodName); } else if (this.defaults.getDestroyMethod() != null) { bd.setDestroyMethodName(this.defaults.getDestroyMethod()); bd.setEnforceDestroyMethod(false); } if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) { bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE)); } if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) { bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE)); } return bd;}通过以上代码我们可以发现, 在1.0版本会有一个singleton 不过呢, 这个很快就被取代掉了, 使用的话会提示错误信息, 在新的版本里, 我们使用了scpoe=“singleton” 作为一个新的使用方式, 我们都知道在spring中,所创建的bean 都被认为默认认为是单例的,并且通过scope这个关键字,即scope=“singleton”。另外scope还有prototype、request、session、global session作用域。scope=“prototype"多例。 scope就是一个作用域,如果有不太理解的话,可以参照一下:Spring scope作用域详解。在这里只是看了一下scope标签的解析, 那么肯定还会有很多的属性, 我们用或者没有用过, 但是就不做讲解了, 我们知道spring是如何去做的就好了。2. 解析bean中的元数据, meta中的内容例如:<bean id=“car” class=“test.CarFactoryBean”> <meta key = “key” value = “values”></bean>通常我们通过配置文件使用spring的时候, 我们会用到meta这个标签,这个标签中门会配置上, key, 和value这两个属性,public void parseMetaElements(Element ele, BeanMetadataAttributeAccessor attributeAccessor) { NodeList nl = ele.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (isCandidateElement(node) && nodeNameEquals(node, META_ELEMENT)) { Element metaElement = (Element) node; String key = metaElement.getAttribute(KEY_ATTRIBUTE); String value = metaElement.getAttribute(VALUE_ATTRIBUTE); BeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value); attribute.setSource(extractSource(metaElement)); attributeAccessor.addMetadataAttribute(attribute); } }}通过代码, 我们可以发现, spring通过解析得到的Element树对象去得到子节点, 然后便利子节点, 并且取得meta元素的key值和value的知值,然后将其存储在attributeAccessor中。由此推测, 其他的子集标签也会是通过这种方式去加载, 并且这也符合我们的预期。n ...

March 11, 2019 · 2 min · jiezi

Spring AOP从零单排-织入时期源码分析

问题:Spring AOP代理中的运行时期,是在初始化时期织入还是获取对象时期织入?织入就是代理的过程,指目标对象进行封装转换成代理,实现了代理,就可以运用各种代理的场景模式。何为AOP简单点来定义就是切面,是一种编程范式。与OOP对比,它是面向切面,为何需要切面,在开发中,我们的系统从上到下定义的模块中的过程中会产生一些横切性的问题,这些横切性的问题和我们的主业务逻辑关系不大,假如不进行AOP,会散落在代码的各个地方,造成难以维护。AOP的编程思想就是把业务逻辑和横切的问题进行分离,从而达到解耦的目的,使代码的重用性、侵入性低、开发效率高。AOP使用场景日志记录;记录调用方法的入参和结果返参。用户的权限验证;验证用户的权限放到AOP中,与主业务进行解耦。性能监控;监控程序运行方法的耗时,找出项目的瓶颈。事务管理;控制Spring事务,Mysql事务等。AOP概念点AOP和Spring AOP的关系在这里问题中,也有一个类似的一对IOC和DI(dependency injection)的关系,AOP可以理解是一种编程目标,Spring AOP就是这个实现这个目标的一种手段。同理IOC也是一种编程目标,DI就是它的一个手段。SpringAOP和AspectJ是什么关系在Spring官网可以看到,AOP的实现提供了两种支持分别为@AspectJ、Schema-based AOP。其实在Spring2.5版本时,Spring自己实现了一套AOP开发的规范和语言,但是这一套规范比较复杂,可读性差。之后,Spring借用了AspectJ编程风格,才有了@AspectJ的方式支持,那么何为编程风格。Annotation注解方式;对应@AspectJJavaConfig;对应Schema-based AOPSpringAOP和AspectJ的详细对比,在之后的章节会在进行更加详细的说明,将会在他们的背景、织入方法、性能做介绍。Spring AOP的应用阅读官网,是我们学习一个新知识的最好途径,这个就是Spring AOP的核心概念点,跟进它们的重要性,我做了重新的排序,以便好理解,这些会为我们后续的源码分析起到作用。Aspect:切面;使用@Aspect注解的Java类来实现,集合了所有的切点,做为切点的一个载体,做一个比喻就像是我们的一个数据库。Tips:这个要实现的话,一定要交给Spirng IOC去管理,也就是需要加入@Component。Pointcut:切点;表示为所有Join point的集合,就像是数据库中一个表。Join point:连接点;俗称为目标对象,具体来说就是servlet中的method,就像是数据库表中的记录。Advice:通知;这个就是before、after、After throwing、After (finally)。Weaving:把代理逻辑加入到目标对象上的过程叫做织入。target:目标对象、原始对象。aop Proxy:代理对象 包含了原始对象的代码和增加后的代码的那个对象。Tips这个应用点,有很多的知识点可以让我们去挖掘,比如Pointcut中execution、within的区别,我相信你去针对性搜索或者官网都未必能有好的解释,稍后会再专门挑一个文章做重点的使用介绍;SpringAOP源码分析为了回答我们的一开始的问题,前面的几个章节我们做了一些简单的概念介绍做为铺垫,那么接下来我们回归正题,正面去切入问题。以码说话,我们以最简洁的思路把AOP实现,我们先上代码。项目结构介绍项目目录结构,比较简单,5个主要的文件;pom.xml核心代码;spring-content是核心jar,已经包含了spring所有的基础jar,aspectjweaver是为了实现AOP。AppConfig.java;定义一个Annotation,做为我们Spirng IOC容器的启动类。package com.will.config;@Configuration@ComponentScan(“com.will”)@EnableAspectJAutoProxy(proxyTargetClass = false)public class AppConfig { }WilAspect.java ;按照官网首推的方式(@AspectJ support),实现AOP代理。package com.will.config;/** * 定义一个切面的载体 /@Aspect@Componentpublic class WilAspect { /* * 定义一个切点 / @Pointcut(“execution( com.will.dao..(..))”) public void pointCutExecution(){ } /** * 定义一个Advice为Before,并指定对应的切点 * @param joinPoint / @Before(“pointCutExecution()”) public void before(JoinPoint joinPoint){ System.out.println(“proxy-before”); }}Dao.javapackage com.will.dao;public interface Dao { public void query();}UserDao.javapackage com.will.dao;import org.springframework.stereotype.Component;@Componentpublic class UserDao implements Dao { public void query() { System.out.println(“query user”); }}Test.javapackage com.will.test;import com.will.config.AppConfig;import com.will.dao.Dao;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class Test { public static void main(String[] args) { /* * new一个注册配置类,启动IOC容器,初始化时期; / AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class); /* * 获取Dao对象,获取对象时期,并进行query打印 */ Dao dao = annotationConfigApplicationContext.getBean(Dao.class); dao.query(); annotationConfigApplicationContext.start(); }}好了,这样我们整体的AOP代理就已经完成。问题分析测试究竟是哪个时期进行对象织入的,比如Test类中,究竟是第一行还是第二行进行织入的,我们只能通过源码进行分析,假如是你,你会进行如何的分析源码解读。Spring的代码非常优秀,同时也非常复杂,那是一个大项目,里面进行了很多的代码封装,那么的代码你三天三夜也读不完,甚至于你都不清楚哪一行的该留意的,哪一行是起到关键性作用的,这里教几个小技巧。看方法返回类型;假如是void返回类型的,看都不看跳过。返回结果是对象,比如T果断进行去进行跟踪。假设法;就当前场景,我们大胆假设是第二行进行的织入。借助好的IDE;IDEA可以帮我们做很多的事情,它的debug模式中的条件断点、调用链(堆栈)会帮助到我们。假设法源码分析debug模式StepInfo(F5)后,进入 AbstractApplicationContext.getBean方法,这个是Spring应用上下文中最重要的一个类,这个抽象类中提供了几乎ApplicationContext的所有操作。这里第一个语句返回void,我们可以直接忽略,看下面的关键性代码。继续debug后,会进入到 DefaultListableBeanFactory 类中,看如下代码return new NamedBeanHolder<>(beanName, getBean(beanName, requiredType, args));在该语句中,这个可以理解为 DefaultListableBeanFactory 容器,帮我们获取相应的Bean。进入到AbstractBeanFactory类的doGetBean方法之后,我们运行完。Object sharedInstance = getSingleton(beanName);语句之后,看到 sharedInstance 对象打印出&Proxyxxx ,说明在getSingleton 方法的时候就已经获取到了对象,所以需要跟踪进入到 getSingleton 方法中,继续探究。不方便不方便我们进行问题追踪到这个步骤之后,我需要引入IDEA的条件断点,不方便我们进行问题追踪因为Spring会初始化很多的Bean,我们再ObjectsharedInstance=getSingleton(beanName);加入条件断点语句。继续debug进入到DefaultSingletonBeanRegistry的getSingleton方法。我们观察下执行完ObjectsingletonObject=this.singletonObjects.get(beanName); 之后的singletonObject已经变成为&ProxyUserDao,这个时候Spring最关键的一行代码出现了,请注意这个this.singletonObjects。this.singletonObjects就是相当IOC容器,反之IOC容器就是一个线程安全的线程安全的HashMap,里面存放着我们需要Bean。我们来看下singletonObjects存放着的数据,里面就有我们的UserDao类。这就说明,我们的初始化的时期进行织入的,上图也有整个Debug模式的调用链。源码深层次探索通过上一个环节已经得知是在第一行进行初始化的,但是它在初始化的时候是什么时候完成织入的,抱着求知的心态我们继续求证。还是那个问题,那么多的代码,我的切入点在哪里?既然singletonObjects是容器,存放我们的Bean,那么找到关键性代码在哪里进行存放(put方法)就可以了。于是我们通过搜索定位到了。我们通过debug模式的条件断点和debug调用链模式,就可以进行探索。这个时候借助上图中的调用链,我们把思路放到放到IDEA帮我定位到的两个方法代码上。DefaultSingletonBeanRegistry.getSingleton我们一步步断点,得知,当运行完singletonObject=singletonFactory.getObject();之后,singletonObject已经获得了代理。至此我们知道,代理对象的获取关键在于singletonFactory对象,于是又定位到了AbstractBeanFactorydoGetBean方法,发现singletonFactory参数是由createBean方法创造的。这个就是Spring中IOC容器最核心的地方了,这个代码的模式也值得我们去学习。sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } });这个第二个参数是用到了jdk8中的lambda,这一段的含义是就是为了传参,重点看下 createBean(beanName,mbd,args);代码。随着断点,我们进入到这个类方法里面。AbstractAutowireCapableBeanFactory.createBean中的;ObjectbeanInstance=doCreateBean(beanName,mbdToUse,args)方法;doCreateBean方法中,做了简化。Initialize the bean instance. Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } … return exposedObject;当运行完 exposedObject=initializeBean(beanName,exposedObject,mbd);之后,我们看到exposedObject已经是一个代理对象,并执行返回。这一行代码就是取判断对象要不要执行代理,要的话就去初始化代理对象,不需要直接返回。后面的initializeBean方法是涉及代理对象生成的逻辑(JDK、Cglib),后续会有一个专门的章节进行详细介绍。总结通过源码分析,我们得知,Spring AOP的代理对象的织入时期是在运行Spring初始化的时候就已经完成的织入,并且也分析了Spring是如何完成的织入。 ...

March 11, 2019 · 2 min · jiezi

如何学习一门新语言或框架

简评:新的语言层出不穷,Dart, Go, Kotlin, Elixir 等等。极光日报曾经分享过一篇文章 —— 不同编程语言的学习曲线。挑战学习曲线这事儿可能太难,但有些小技巧能帮助我们快速学习。原作者 Vinicius Brasil 分享了以下几点 ~1、先掌握语言,再学习框架有些朋友倾向于学习框架,比如 Ruby on Rails。框架很多,但都是在语言的基础上发展的,掌握了基础才能更快适应新的技术。2、编程挑战在 LeetCode,HackerRank 和 Project Euler 等网站上刷题,一方面提高自己的编程能力,一方面也为面试做了准备。3、充分使用 Stack Overflow 和 Code Review编程挑战遇到问题时,你可以使用 StackOverflow,这是一个不需要介绍的网站。同时呢,积极审查自己的代码并加以优化。4、阅读大量代码GitHub 是个好东西。从经典的代码中学习代码的规范与好的编码习惯。例如 快排的三数中值法。5、安装合适的编译器插件Linters 是代码分析工具,用于标记错误,你可以用它检查自己的语法错误并加以分析。6、 知识迁移学会对比不同的语言的代码各自的特性,找出相同之处,提高自己的学习效率。比如说这段 Python 代码:def matrix_of_floats(matrix_of_anything): n = len(matrix_of_anything) n_i = len(matrix_of_anything[0]) new_matrix_of_floats = [] for i in xrange(0, n): row = [] for j in xrange(0, n_i): row.append(float(matrix_of_anything[i][j])) new_matrix_of_floats.append(row) return new_matrix_of_floatsPythonic 方式:def matrix_of_floats(matrix_of_anything): return [[float(a_ij) for a_ij in a_i] for a_i in matrix_of_anything]7、实现一些东西在功能实现的同时找到编程语言的乐趣,给编程语言找到具体的应用场景。8、不要放弃编程的学习是一个漫长的过程,编程的学习是无法穷尽的,我们要做的就是在学习的过程中找到我们的乐趣。原文链接:How to Learn a New Programming Language or Framework ...

March 11, 2019 · 1 min · jiezi

SpringBoot基础篇AOP之高级使用技能

更多相关内容,查看: http://spring.hhui.top/前面一篇博文 190301-SpringBoot基础篇AOP之基本使用姿势小结 介绍了aop的简单使用方式,在文章最后,抛出了几个问题待解决,本篇博文则将针对前面的问题,看下更多关于AOP的使用说明<!– more –>I. 高级技能1. 注解拦截方式前面一文,主要介绍的是根据正则表达式来拦截对应的方法,接下来演示下如何通过注解的方式来拦截目标方法,实现也比较简单首先创建注解@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface AnoDot {}接着在目标方法上添加注解,这里借助前面博文中工程进行说明,新建一个com.git.hui.boot.aop.demo2.AnoDemoBean,注意这个包路径,是不会被前文的AnoAspect定义的Advice拦截的,这里新建一个包路径的目的就是为了尽可能的减少干扰项@Componentpublic class AnoDemoBean { @AnoDot public String genUUID(long time) { try { System.out.println(“in genUUID before process!”); return UUID.randomUUID() + “|” + time; } finally { System.out.println(“in genUUID finally!”); } }}接下来定义对应的advice, 直接在前面的AnoAspect中添加(不知道前文的也没关系,下面贴出相关的代码类,前文的类容与本节内容无关)@Aspect@Componentpublic class AnoAspect { @Before("@annotation(AnoDot)") public void anoBefore() { System.out.println(“AnoAspect “); }}测试代码@SpringBootApplicationpublic class Application { private AnoDemoBean anoDemoBean; public Application(AnoDemoBean anoDemoBean) { this.anoDemoBean = anoDemoBean; this.anoDemoBean(); } private void anoDemoBean() { System.out.println(">>>>>>>” + anoDemoBean.genUUID(System.currentTimeMillis())); } public static void main(String[] args) { SpringApplication.run(Application.class); }}输出结果如下,在执行目标方法之前,会先执行before advice中的逻辑AnoAspect in genUUID before process!in genUUID finally!>>>>>>>3a5d749d-d94c-4fc0-a7a3-12fd97f3e1fa|15515134436442. 多个advice拦截一个方法执行时,如果有多个advice满足拦截规则,是所有的都会触发么?通过前面一篇博文知道,不同类型的advice是都可以拦截的,如果出现多个相同类型的advice呢?在前面一篇博文的基础上进行操作,我们扩展下com.git.hui.boot.aop.demo.DemoBean@Componentpublic class DemoBean { @AnoDot public String genUUID(long time) { try { System.out.println(“in genUUID before process!”); return UUID.randomUUID() + “|” + time; } finally { System.out.println(“in genUUID finally!”); } }}对应的测试切面内容如@Aspect@Componentpublic class AnoAspect { @Before(“execution(public * com.git.hui.boot.aop.demo..())”) public void doBefore(JoinPoint joinPoint) { System.out.println(“do in Aspect before method called! args: " + JSON.toJSONString(joinPoint.getArgs())); } @Pointcut(“execution(public * com.git.hui.boot.aop.demo..())”) public void point() { } @After(“point()”) public void doAfter(JoinPoint joinPoint) { System.out.println(“do in Aspect after method called! args: " + JSON.toJSONString(joinPoint.getArgs())); } /** * 执行完毕之后,通过 args指定参数;通过 returning 指定返回的结果,要求返回值类型匹配 * * @param time * @param result */ @AfterReturning(value = “point() && args(time)”, returning = “result”) public void doAfterReturning(long time, String result) { System.out.println(“do in Aspect after method return! args: " + time + " ans: " + result); } @Around(“point()”) public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println(“do in Aspect around —— before”); Object ans = joinPoint.proceed(); System.out.println(“do in Aspect around ——- over! ans: " + ans); return ans; } @Before(“point()”) public void sameBefore() { System.out.println(“SameAspect”); } @Before("@annotation(AnoDot)”) public void anoBefore() { System.out.println(“AnoAspect”); }}测试代码如下@SpringBootApplicationpublic class Application { private DemoBean demoBean; public Application(DemoBean demoBean) { this.demoBean = demoBean; this.demoBean(); } private void demoBean() { System.out.println(">>>>> " + demoBean.genUUID(System.currentTimeMillis())); } public static void main(String[] args) { SpringApplication.run(Application.class); }}输出结果如下,所有的切面都执行了,也就是说,只要满足条件的advice,都会被拦截到do in Aspect around —— beforeAnoAspectdo in Aspect before method called! args: [1551520547268]SameAspectin genUUID before process!in genUUID finally!do in Aspect around ——- over! ans: 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|1551520547268do in Aspect after method called! args: [1551520547268]do in Aspect after method return! args: 1551520547268 ans: 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|1551520547268>>>>> 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|15515205472683. 嵌套拦截嵌套的方式有几种case,先看第一种a. 调用方法不满足拦截规则,调用本类中其他满足拦截条件的方法这里我们借助第一节中的bean来继续模拟, 在AnoDemoBean类中,新增一个方法@Componentpublic class AnoDemoBean { public String randUUID(long time) { try { System.out.println(“in randUUID start!”); return genUUID(time); } finally { System.out.println(“in randUUID finally!”); } } @AnoDot public String genUUID(long time) { try { System.out.println(“in genUUID before process!”); return UUID.randomUUID() + “|” + time; } finally { System.out.println(“in genUUID finally!”); } }}对应的切面为@Aspect@Componentpublic class NetAspect { @Around("@annotation(AnoDot)”) public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println(“In NetAspect doAround before!”); Object ans = joinPoint.proceed(); System.out.println(“In NetAspect doAround over! ans: " + ans); return ans; }}然后测试case需要改为直接调用 AnoDemoBean#randUUID,需要看这个方法内部调用的genUUID是否会被切面拦截住@SpringBootApplicationpublic class Application { private AnoDemoBean anoDemoBean; public Application(AnoDemoBean anoDemoBean) { this.anoDemoBean = anoDemoBean; this.anoDemoBean(); } private void anoDemoBean() { System.out.println(">>>>>>>” + anoDemoBean.randUUID(System.currentTimeMillis())); } public static void main(String[] args) { SpringApplication.run(Application.class); }}输出结果如下,没有切面的日志,表明这种场景下,不会被拦截in randUUID start!in genUUID before process!in genUUID finally!in randUUID finally!>>>>>>>0c6a5ccf-30c0-4ac0-97f2-3dc063580f3d|1551522176035b. 调用方法不满足拦截规则,调用其他类中满足拦截条件的方法依然使用前面的例子进行说明,不过是稍稍改一下AnoDemoBean,调用第二节中的DemoBean的方法DemoBean的代码如下@AnoDotpublic String genUUID(long time) { try { System.out.println(“in DemoBean genUUID before process!”); return UUID.randomUUID() + “|” + time; } finally { System.out.println(“in DemoBean genUUID finally!”); }}然后AnoDemoBean的代码如下@Componentpublic class AnoDemoBean { @Autowired private DemoBean demoBean; public String randUUID(long time) { try { System.out.println(“in AnoDemoBean randUUID start!”); return genUUID(time) + “<<<>>>” + demoBean.genUUID(time); } finally { System.out.println(“in AnoDemoBean randUUID finally!”); } } @AnoDot public String genUUID(long time) { try { System.out.println(“in AnoDemoBean genUUID before process!”); return UUID.randomUUID() + “|” + time; } finally { System.out.println(“in AnoDemoBean genUUID finally!”); } }}测试代码和前面完全一致,接下来看下输出in AnoDemoBean randUUID start!in AnoDemoBean genUUID before process!in AnoDemoBean genUUID finally!### 上面三行为 anoDemoBean#randUUID方法调用 anoDemoBean#genUUID方法的输出结果,可以看到没有切面执行的日志输出### 下面的为调用 demoBean#genUUID 方法,可以看到切面(NetAspect#doAround)执行的日志In NetAspect doAround before!in DemoBean genUUID before process!in DemoBean genUUID finally!In NetAspect doAround over! ans: f35b8878-fbd0-4840-8fbe-5fef8eda5e31|1551522532092### 最后是收尾in AnoDemoBean randUUID finally!>>>>>>>e516a35f-b85a-4cbd-aae0-fa97cdecab47|1551522532092<<<>>>f35b8878-fbd0-4840-8fbe-5fef8eda5e31|1551522532092从上面的日志分析中,可以明确看出对比,调用本类中,满足被拦截的方法,也不会走切面逻辑;调用其他类中的满足切面拦截的方法,会走切面逻辑c. 调用方法满足切面拦截条件,又调用其他满足切面拦截条件的方法这个和两个case有点像,不同的是直接调用的方法也满足被切面拦截的条件,我们主要关注点在于嵌套调用的方法,会不会进入切面逻辑,这里需要修改的地方就很少了,直接把 AnoDemoBean#randUUID方法上添加注解,然后执行即可@Componentpublic class AnoDemoBean { @Autowired private DemoBean demoBean; @AnoDot public String randUUID(long time) { try { System.out.println(“in AnoDemoBean randUUID start!”); return genUUID(time) + “<<<>>>” + demoBean.genUUID(time); } finally { System.out.println(“in AnoDemoBean randUUID finally!”); } } @AnoDot public String genUUID(long time) { try { System.out.println(“in AnoDemoBean genUUID before process!”); return UUID.randomUUID() + “|” + time; } finally { System.out.println(“in AnoDemoBean genUUID finally!”); } }}输出结果如下## 最外层的切面拦截的是 AnoDemoBean#randUUID 方法的执行In NetAspect doAround before!in AnoDemoBean randUUID start!in AnoDemoBean genUUID before process!in AnoDemoBean genUUID finally!### 从跟上面三行的输出,可以知道内部调用的 AnoDemoBean#genUUID 即便满足切面拦截规则,也不会再次走切面逻辑### 下面4行,表明其他类的方法,如果满足切面拦截规则,会进入到切面逻辑In NetAspect doAround before!in DemoBean genUUID before process!in DemoBean genUUID finally!In NetAspect doAround over! ans: d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801in AnoDemoBean randUUID finally!In NetAspect doAround over! ans: cf350bc2-9a9a-4ef6-b496-c913d297c960|1551522969801<<<>>>d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801>>>>>>>cf350bc2-9a9a-4ef6-b496-c913d297c960|1551522969801<<<>>>d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801从输出结果进行反推,一个结论是执行的目标方法,如果调用了本类中一个满足切面规则的方法A时,在执行方法A的过程中,不会触发切面逻辑执行的目标方法,如果调用其他类中一个满足切面规则的方法B时,在执行方法B的过程中,将会触发切面逻辑4. AOP拦截方法作用域前面测试的被拦截方法都是public,那么是否表明只有public方法才能被拦截呢?从第三节基本可以看出,private方法首先淘汰出列,为啥?因为private方法正常来讲只能内部调用,而内部调用不会走切面逻辑;所以接下来需要关注的主要放在默认作用域和protected作用域@Componentpublic class ScopeDemoBean { @AnoDot String defaultRandUUID(long time) { try { System.out.println(” in ScopeDemoBean defaultRandUUID before!”); return UUID.randomUUID() + " | default | " + time; } finally { System.out.println(” in ScopeDemoBean defaultRandUUID finally!"); } } @AnoDot protected String protectedRandUUID(long time) { try { System.out.println(" in ScopeDemoBean protectedRandUUID before!"); return UUID.randomUUID() + " | protected | " + time; } finally { System.out.println(" in ScopeDemoBean protectedRandUUID finally!"); } } @AnoDot private String privateRandUUID(long time) { try { System.out.println(" in ScopeDemoBean privateRandUUID before!"); return UUID.randomUUID() + " | private | " + time; } finally { System.out.println(" in ScopeDemoBean privateRandUUID finally!"); } }}我们不直接使用这个类里面的方法,借助前面的 AnoDemoBean, 下面给出了通过反射的方式来调用private方法的case@Componentpublic class AnoDemoBean { @Autowired private ScopeDemoBean scopeDemoBean; public void scopeUUID(long time) { try { System.out.println("——– default ——–"); String defaultAns = scopeDemoBean.defaultRandUUID(time); System.out.println("——– default: " + defaultAns + " ——–\n"); System.out.println("——– protected ——–"); String protectedAns = scopeDemoBean.protectedRandUUID(time); System.out.println("——– protected: " + protectedAns + " ——–\n"); System.out.println("——– private ——–"); Method method = ScopeDemoBean.class.getDeclaredMethod(“privateRandUUID”, long.class); method.setAccessible(true); String privateAns = (String) method.invoke(scopeDemoBean, time); System.out.println("——– private: " + privateAns + " ——–\n"); } catch (Exception e) { e.printStackTrace(); } }}测试case@SpringBootApplicationpublic class Application { private AnoDemoBean anoDemoBean; public Application(AnoDemoBean anoDemoBean) { this.anoDemoBean = anoDemoBean; this.anoDemoBean(); } private void anoDemoBean() { anoDemoBean.scopeUUID(System.currentTimeMillis()); } public static void main(String[] args) { SpringApplication.run(Application.class); }}输出结果如下,从日志打印来看,protected和default方法的切面都走到了——– default ——–In NetAspect doAround before! in ScopeDemoBean defaultRandUUID before! in ScopeDemoBean defaultRandUUID finally!In NetAspect doAround over! ans: 2ad7e509-c62c-4f25-b68f-eb5e0b53196d | default | 1551524311537——– default: 2ad7e509-c62c-4f25-b68f-eb5e0b53196d | default | 1551524311537 —————- protected ——–In NetAspect doAround before! in ScopeDemoBean protectedRandUUID before! in ScopeDemoBean protectedRandUUID finally!In NetAspect doAround over! ans: 9eb339f8-9e71-4321-ab83-a8953d1b8ff8 | protected | 1551524311537——– protected: 9eb339f8-9e71-4321-ab83-a8953d1b8ff8 | protected | 1551524311537 —————- private ——– in ScopeDemoBean privateRandUUID before! in ScopeDemoBean privateRandUUID finally!——– private: 1826afac-6eca-4dc3-8edc-b4ca7146ce28 | private | 1551524311537 ——–5. 小结本篇博文篇幅比较长,主要是测试代码比较占用地方,因此有必要简单的小结一下,做一个清晰的归纳,方便不想看细节,只想获取最终结论的小伙伴注解拦截方式:首先声明注解在目标方法上添加注解切面中,advice的内容形如 @Around("@annotation(AnoDot)")多advice情况:多个advice满足拦截场景时,全部都会执行嵌套场景执行的目标方法,如果调用了本类中一个满足切面规则的方法A时,在执行方法A的过程中,不会触发切面逻辑执行的目标方法,如果调用其他类中一个满足切面规则的方法B时,在执行方法B的过程中,将会触发切面逻辑作用域public, protected, default 作用域的方法都可以被拦截优先级这个内容因为特别多,所以有必要单独拎出来,其主要的分类如下同一aspect,不同advice的执行顺序不同aspect,advice的执行顺序同一aspect,相同advice的执行顺序II. 其他0. 项目工程:https://github.com/liuyueyi/spring-boot-demo项目: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/010-aop1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

March 10, 2019 · 5 min · jiezi

使用spring validation完成数据后端校验

前言 简述JSR303/JSR-349,hibernate validation,spring validation之间的关系。JSR303是一项标准,JSR-349是其的升级版本,添加了一些新特性,他们规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,他们位于javax.validation.constraints包下,只提供规范不提供实现。而hibernate validation是对这个规范的实践(不要将hibernate和数据库orm框架联系在一起),他提供了相应的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等,他们位于org.hibernate.validator.constraints包下。而万能的spring为了给开发者提供便捷,对hibernate validation进行了二次封装,显示校验validated bean时,你可以使用spring validation或者hibernate validation,而spring validation另一个特性,便是其在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中。这无疑便捷了我们的web开发。本文主要介绍在springmvc中自动校验的机制。2. 常用的校验方式限制说明@Null限制只能为null@NotNull限制必须不为null@AssertFalse限制必须为false@AssertTrue限制必须为true@DecimalMax(value)限制必须为一个不大于指定值的数字@DecimalMin(value)限制必须为一个不小于指定值的数字@Digits(integer,fraction)限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction@Future限制必须是一个将来的日期@Max(value)限制必须为一个不大于指定值的数字@Min(value)限制必须为一个不小于指定值的数字@Past限制必须是一个过去的日期@Pattern(value)限制必须符合指定的正则表达式@Size(max,min)限制字符长度必须在min到max之间@Past验证注解的元素值(日期类型)比当前时间早@NotEmpty验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)@NotBlank验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格@Email验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式更多方式请查看源代码路径下提供的全量校验方式。3. 自定义注解添加校验方法例如:我们校验手机号或身份证号,官方提供的注解中没有支持的,当然我们可以通过官方提供的正则表达式来校验:@Pattern(regexp = “^1(3|4|5|7|8)\d{9}$",message = “手机号码格式错误”)@NotBlank(message = “手机号码不能为空”)private String phone;但是这种方式并不是很方便,我们可以自定义一个校验规则的注解3.1 定义手机号校验注解 @Phone@Target({ ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Constraint(validatedBy = PhoneValidator.class)public @interface Phone { /** * 校验不通过的message / String message() default “请输入正确的手机号”; /* * 分组校验 / Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { };}3.2 定义校验方式public class PhoneValidator implements ConstraintValidator<Phone, String> { @Override public void initialize(Phone constraintAnnotation) { } @Override public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) { if(!StringUtils.isEmpty(phone)){ //获取默认提示信息 String defaultConstraintMessageTemplate = constraintValidatorContext.getDefaultConstraintMessageTemplate(); System.out.println(“default message :” + defaultConstraintMessageTemplate); //禁用默认提示信息 constraintValidatorContext.disableDefaultConstraintViolation(); //设置提示语 constraintValidatorContext.buildConstraintViolationWithTemplate(“手机号格式错误”).addConstraintViolation(); String regex = “^1(3|4|5|7|8)\d{9}$”; return phone.matches(regex); } return true; }}4. 引入依赖我们使用maven构建springboot应用来进行demo演示。<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies>我们只需要引入spring-boot-starter-web依赖即可,如果查看其子依赖,可以发现如下的依赖:<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId></dependency><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId></dependency>验证了我之前的描述,web模块使用了hibernate-validation,并且databind模块也提供了相应的数据绑定功能。5. 构建简单Demo项目5.1 构建启动类无需添加其他注解,一个典型的启动类@SpringBootApplicationpublic class ValidateApp { public static void main(String[] args) { SpringApplication.run(ValidateApp.class, args); }}5.2 创建需要被校验的实体类@Datapublic class UserEntity { @NotBlank private String name; @Range(max = 150, min = 1, message = “年龄范围应该在1-150内。”) private Integer age; @Email(message = “邮箱格式错误”) private String email; @NotEmpty(message = “密码不能为空”) @Length(min = 6, max = 8, message = “密码长度为6-8位。”) private String password; @Pattern(regexp = “^1(3|4|5|7|8)\d{9}$",message = “手机号码格式错误”) @NotBlank(message = “手机号码不能为空”) private String phone; @IdCard private String idCard;}5.3 在Controller中开启校验在Controller 中 请求参数上添加@Validated 标签开启验证@RestController@Slf4jpublic class TestController { @PostMapping("/user”) public String test1(@RequestBody @Validated UserEntity userEntity){ log.info(“user is {}",userEntity); return “success”; }}5.4 校验结果{ “timestamp”: “2019-03-10T09:29:20.978+0000”, “status”: 400, “error”: “Bad Request”, “errors”: [ { “codes”: [ “NotBlank.userEntity.name”, “NotBlank.name”, “NotBlank.java.lang.String”, “NotBlank” ], “arguments”: [ { “codes”: [ “userEntity.name”, “name” ], “arguments”: null, “defaultMessage”: “name”, “code”: “name” } ], “defaultMessage”: “不能为空”, “objectName”: “userEntity”, “field”: “name”, “rejectedValue”: “”, “bindingFailure”: false, “code”: “NotBlank” } ], “message”: “Validation failed for object=‘userEntity’. Error count: 1”, “path”: “/user”}这个结果我们可以进行统一处理,筛选出适合给前端返回的错误提示文案。关于统一处理异常,在这篇文章中已经提到:https://segmentfault.com/a/11…5.5 异常处理@ControllerAdvice@ResponseBody@Slf4jpublic class GlobalExceptionHandler { /* * 分隔符 / private static final String SEPARATOR = “,”; /* * 拦截数据校验异常 * * @param request 请求 * @param e 校验异常 * @return 通用返回格式 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ZingResult notValidException(HttpServletRequest request, MethodArgumentNotValidException e) { log.error(“请求的url为{}出现数据校验异常,异常信息为:”, request.getRequestURI(), e); BindingResult bindingResult = e.getBindingResult(); List<String> errorMsgList = new ArrayList(); for (FieldError fieldError : bindingResult.getFieldErrors()) { errorMsgList.add(fieldError.getDefaultMessage()); } return ZingResult.error(ExceptionEnum.PARAM_ERROR,errorMsgList); }}6. 总结这个框架校验还有其他多种用法,如,分组校验、手动校验等,我总结的这篇博客也是参照该文章。详情参看:https://blog.csdn.net/u013815…最后该作者的总结也非常好:我推崇的方式,是仅仅使用自带的注解和自定义注解,完成一些简单的,可复用的校验。寻求一个易用性和封装复杂性之间的平衡点是我们作为工具使用者应该考虑的。

March 10, 2019 · 2 min · jiezi

重拾-Spring Transaction

问题面试中是不是有时经常会被问到 “Spring 事务如何管理的了解吗?” ,“Spring 事务的传播性有哪些,能聊聊它们的使用场景吗?”, “事务回滚的时候是所有异常下都会回滚吗?”; 下面我们就带着这些问题来看看 Spring 事务是如何实现的吧。实现分析首先我们还是先通过一个使用示例,先看下 Spring 事务是如何工作的。使用示例本文我们先采用 TransactionProxyFactoryBean 配置的方式来看下, Spring 事务如何实现<beans> <!– 配置数据源 –> <bean id=“dataSource” class=“org.apache.commons.dbcp.BasicDataSource” > <property name=“driverClassName”> <value>com.mysql.jdbc.Driver</value> </property> <property name=“url”> <value>url</value> </property> <property name=“username”> <value>username</value> </property> <property name=“password”> <value>password</value> </property> </bean> <!– 配置事务管理 –> <bean id=“transactionManager” class=“org.springframework.jdbc.datasource.DataSourceTransactionManager”> <property name=“dataSource”> <ref bean=“dataSource”></ref> </property> </bean> <!– 配置 jdbcTemplate –> <bean id=“jdbcTemplate” class=“org.springframework.jdbc.core.JdbcTemplate”> <property name=“dataSource”> <ref bean=“dataSource”/> </property> </bean> <bean id=“userServiceTarget” class=“org.springframework.transaction.UserServiceImpl”> <property name=“jdbcTemplate”> <ref bean=“jdbcTemplate”></ref> </property> </bean> <!– TransactionProxyFactoryBean 实现了接口 InitializingBean,在初始化过程中会调用 afterPropertiesSet 1 : 创建事务拦截器 2 : 创建事务 advisor (事务的拦截切面) 3 : 创建代理 –> <bean id=“userService” class=“org.springframework.transaction.interceptor.TransactionProxyFactoryBean”> <property name=“transactionManager”> <ref bean=“transactionManager” /> </property> <property name=“target”> <ref bean=“userServiceTarget”></ref> </property> <property name=“proxyInterfaces”> <value>org.springframework.transaction.UserService</value> </property> <!– 配置事务属性 传播行为, 事务隔离级别, 是否只读, 回滚规则(哪些异常下执行回滚),key 为配置需要事务管理的方法名; 在代理目标执行的时候会通过该属性判断方法是否需要事务管理 –> <property name=“transactionAttributes”> <props> <prop key="">PROPAGATION_REQUIRED</prop> </props> </property> </bean></beans>在 TransactionProxyFactoryBean 的属性配置中如果您对 transactionAttributes 属性不熟悉的话,是不是会感觉一头雾水呢? 这个玩意怎么配置的? 配置格式又是什么样的呢? 配置值有哪些呢 ?; 下面将会通过对 TransactionProxyFactoryBean 的源码分析来一一解答。源码分析类结构从 TransactionProxyFactoryBean 类结构我们知道,其实现了接口 InitializingBean 和 FactoryBean; 那么也就是在 TransactionProxyFactoryBean 实例化后会调用方法 afterPropertiesSet, 在获取目标对象实例时会调用方法 getObject; 下面将主要看下这两个方法的实现。afterPropertiesSet-创建目标代理对象public void afterPropertiesSet() throws AopConfigException { // 校验 Target 目标对象 if (this.target == null) { throw new AopConfigException(“Target must be set”); } // 校验事务属性定义,从抛出的异常信息可以看出 Spring 在此做了强校验; // 也就是说如果没有需要 Spring 事务管理的方法,就不要采用 TransactionProxyFactoryBean 了 // 那么 transactionAttributeSource 是怎么来的呢? 见下文分析 if (this.transactionAttributeSource == null) { throw new AopConfigException(“Either ’transactionAttributeSource’ or ’transactionAttributes’ is required: " + “If there are no transactional methods, don’t use a transactional proxy.”); } // 创建事务拦截器 transactionInterceptor TransactionInterceptor transactionInterceptor = new TransactionInterceptor(); transactionInterceptor.setTransactionManager(this.transactionManager); transactionInterceptor.setTransactionAttributeSource(this.transactionAttributeSource); transactionInterceptor.afterPropertiesSet(); ProxyFactory proxyFactory = new ProxyFactory(); // 是否配置了前置拦截 if (this.preInterceptors != null) { for (int i = 0; i < this.preInterceptors.length; i++) { proxyFactory.addAdvisor(GlobalAdvisorAdapterRegistry.getInstance().wrap(this.preInterceptors[i])); } } if (this.pointcut != null) { // 如果配置了 pointcut 切入点,则按配置的 pointcut 创建 advisor Advisor advice = new DefaultPointcutAdvisor(this.pointcut, transactionInterceptor); proxyFactory.addAdvisor(advice); } else { // rely on default pointcut // 创建事务拦截切面 advisor proxyFactory.addAdvisor(new TransactionAttributeSourceAdvisor(transactionInterceptor)); // could just do the following, but it’s usually less efficient because of AOP advice chain caching // proxyFactory.addInterceptor(transactionInterceptor); } // 是否配置了后置拦截 if (this.postInterceptors != null) { for (int i = 0; i < this.postInterceptors.length; i++) { proxyFactory.addAdvisor(GlobalAdvisorAdapterRegistry.getInstance().wrap(this.postInterceptors[i])); } } proxyFactory.copyFrom(this); proxyFactory.setTargetSource(createTargetSource(this.target)); // 设置代理的接口 if (this.proxyInterfaces != null) { proxyFactory.setInterfaces(this.proxyInterfaces); } else if (!getProxyTargetClass()) { // rely on AOP infrastructure to tell us what interfaces to proxy proxyFactory.setInterfaces(AopUtils.getAllInterfaces(this.target)); } // 创建目标对象的代理对象 this.proxy = proxyFactory.getProxy();}从源码中我们知道 afterPropertiesSet 主要做以下几件事:参数有效性校验; 校验目标对象,事务属性定义设置代理的 advisor chain, 包括用户自定义的前置拦截, 内置的事务拦截器,用户自定义的后置拦截创建目标代理对象在 afterPropertiesSet 的实现中有个针对 transactionAttributeSource 的非空校验,那么这个变量是何时赋值的呢 ? 还记得使用示例中的关于事务属性的定义 transactionAttributes 吗 ?setTransactionAttributes-设置事务属性定义public void setTransactionAttributes(Properties transactionAttributes) { NameMatchTransactionAttributeSource tas = new NameMatchTransactionAttributeSource(); tas.setProperties(transactionAttributes); this.transactionAttributeSource = tas;}public void setProperties(Properties transactionAttributes) { TransactionAttributeEditor tae = new TransactionAttributeEditor(); // 遍历 properties for (Iterator it = transactionAttributes.keySet().iterator(); it.hasNext(); ) { // key 为匹配的方法名 String methodName = (String) it.next(); String value = transactionAttributes.getProperty(methodName); // 解析 value tae.setAsText(value); TransactionAttribute attr = (TransactionAttribute) tae.getValue(); // 将方法名与事务属性定义匹配关联 addTransactionalMethod(methodName, attr); }}下面我们就看下 setAsText 方法是如何解析事务属性的配置/** * Format is PROPAGATION_NAME,ISOLATION_NAME,readOnly,+Exception1,-Exception2. * Null or the empty string means that the method is non transactional. * @see java.beans.PropertyEditor#setAsText(java.lang.String) /public void setAsText(String s) throws IllegalArgumentException { if (s == null || “".equals(s)) { setValue(null); } else { // tokenize it with “,” // 按 , 分割配置信息 String[] tokens = StringUtils.commaDelimitedListToStringArray(s); RuleBasedTransactionAttribute attr = new RuleBasedTransactionAttribute(); for (int i = 0; i < tokens.length; i++) { String token = tokens[i]; // 以 PROPAGATION 开头,则配置事务传播性 if (token.startsWith(TransactionDefinition.PROPAGATION_CONSTANT_PREFIX)) { attr.setPropagationBehaviorName(tokens[i]); } // 以 ISOLATION 开头,则配置事务隔离级别 else if (token.startsWith(TransactionDefinition.ISOLATION_CONSTANT_PREFIX)) { attr.setIsolationLevelName(tokens[i]); } // 以 timeout_ 开头,则设置事务超时时间 else if (token.startsWith(DefaultTransactionAttribute.TIMEOUT_PREFIX)) { String value = token.substring(DefaultTransactionAttribute.TIMEOUT_PREFIX.length()); attr.setTimeout(Integer.parseInt(value)); } // 若等于 readOnly 则配置事务只读 else if (token.equals(DefaultTransactionAttribute.READ_ONLY_MARKER)) { attr.setReadOnly(true); } // 以 + 开头,则配置哪些异常下不回滚 else if (token.startsWith(DefaultTransactionAttribute.COMMIT_RULE_PREFIX)) { attr.getRollbackRules().add(new NoRollbackRuleAttribute(token.substring(1))); } // 以 - 开头,则配置哪些异常下回滚 else if (token.startsWith(DefaultTransactionAttribute.ROLLBACK_RULE_PREFIX)) { attr.getRollbackRules().add(new RollbackRuleAttribute(token.substring(1))); } else { throw new IllegalArgumentException(“Illegal transaction token: " + token); } } setValue(attr); }}从 setAsText 方法的实现我们就可以搞明白在配置文件中 transactionAttributes 如何配置了,譬如:<property name=“transactionAttributes”> <props> <prop key=”">PROPAGATION_REQUIRED, ISOLATION_DEFAULT, readOnly</prop> </props></property>也可以这样配置:<property name=“transactionAttributes”> <props> <prop key=”">readOnly, ISOLATION_DEFAULT, PROPAGATION_REQUIRED</prop> </props></property>也就是说 transactionAttributes 的配置只要保证 token 格式正确即可,顺序无关;但是从规范来讲建议还是保持 PROPAGATION_NAME,ISOLATION_NAME,readOnly,+Exception1,-Exception2. 的格式。getObject-获取代理对象public Object getObject() { // proxy 对象在 afterPropertiesSet 方法执行时产生 return this.proxy;}代理执行是否支持事务在 重拾-Spring AOP 中我们知道,当代理对象在执行的时候会先获取当前方法所匹配的 advisor (参见类 JdkDynamicAopProxy); 而 TransactionProxyFactoryBean 在创建代理对象的时候会将 TransactionInterceptor 绑定到 TransactionAttributeSourceAdvisor 上,那么我就看下 TransactionAttributeSourceAdvisor 是如何匹配方法的。public class TransactionAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor { private TransactionAttributeSource transactionAttributeSource; public TransactionAttributeSourceAdvisor(TransactionInterceptor ti) { super(ti); if (ti.getTransactionAttributeSource() == null) { throw new AopConfigException(“Cannot construct a TransactionAttributeSourceAdvisor using a " + “TransactionInterceptor that has no TransactionAttributeSource configured”); } this.transactionAttributeSource = ti.getTransactionAttributeSource(); } public boolean matches(Method m, Class targetClass) { return (this.transactionAttributeSource.getTransactionAttribute(m, targetClass) != null); }}TransactionAttributeSourceAdvisor 判断方法是否匹配时,实际是由 NameMatchTransactionAttributeSource 的方法 getTransactionAttribute 来处理。public TransactionAttribute getTransactionAttribute(Method method, Class targetClass) { // 获取目标方法名 String methodName = method.getName(); // 获取目标方法匹配的事务属性定义 TransactionAttribute attr = (TransactionAttribute) this.nameMap.get(methodName); // 如果 attr 不为空说明当前方法配置了事务属性定义,也就是当前方法需要事务管理 if (attr != null) { return attr; } else { // look up most specific name match String bestNameMatch = null; for (Iterator it = this.nameMap.keySet().iterator(); it.hasNext();) { // 判断当前方法是否匹配通配符的方式 String mappedName = (String) it.next(); if (isMatch(methodName, mappedName) && (bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) { attr = (TransactionAttribute) this.nameMap.get(mappedName); bestNameMatch = mappedName; } } return attr; }}protected boolean isMatch(String methodName, String mappedName) { return (mappedName.endsWith(””) && methodName.startsWith(mappedName.substring(0, mappedName.length() - 1))) || (mappedName.startsWith("") && methodName.endsWith(mappedName.substring(1, mappedName.length())));}TransactionInterceptor-事务拦截在完成判断当前方法是否需要事务管理后,如果需要事务管理最终会调用 TransactionInterceptor 执行事务拦截的处理。public final Object invoke(MethodInvocation invocation) throws Throwable { Class targetClass = (invocation.getThis() != null) ? invocation.getThis().getClass() : null; // if the transaction attribute is null, the method is non-transactional // 获取当前方法所支持的事务配置属性,若不存在则说明当前方法不需要事务管理 TransactionAttribute transAtt = this.transactionAttributeSource.getTransactionAttribute(invocation.getMethod(), targetClass); TransactionStatus status = null; TransactionStatus oldTransactionStatus = null; // create transaction if necessary if (transAtt != null) { // the transaction manager will flag an error if an incompatible tx already exists // 通过事务管理获取事务,该事务可能是新创建的也可能是当前上下文已存在的事务 // 返回事务状态 status = this.transactionManager.getTransaction(transAtt); // make the TransactionStatus available to callees oldTransactionStatus = (TransactionStatus) currentTransactionStatus.get(); currentTransactionStatus.set(status); } else { // it isn’t a transactional method } Object retVal = null; try { // 目标方法执行 retVal = invocation.proceed(); } catch (Throwable ex) { // target invocation exception if (status != null) { // 异常处理 可能会执行事务的回滚 onThrowable(invocation, transAtt, status, ex); } throw ex; } finally { if (transAtt != null) { // use stack to restore old transaction status if one was set currentTransactionStatus.set(oldTransactionStatus); } } if (status != null) { // 通过事务管理执行事务提交 this.transactionManager.commit(status); } return retVal;}private void onThrowable(MethodInvocation invocation, TransactionAttribute txAtt, TransactionStatus status, Throwable ex) { // 判断异常是否需要回滚 if (txAtt.rollbackOn(ex)) { try { // 通过事务管理执行回滚 this.transactionManager.rollback(status); } catch (TransactionException tex) { logger.error(“Application exception overridden by rollback exception”, ex); throw tex; } } else { // Will still roll back if rollbackOnly is true // 异常不需要回滚的话 则提交事务 this.transactionManager.commit(status); }}从 TransactionInterceptor 的处理逻辑来看,我们知道其主要做以下事情:获取当前方法所定义的事务属性通过事务管理器 Transaction Manager 来获取事务目标方法执行执行异常处理,如异常需要回滚则通过事务管理器执行事务 rollback,反之执行事务 commit方法执行成功则执行事务 commit也就是说 TransactionInterceptor (事务拦截器) 主要是将事务相关的动作委托给 TransactionManager (事务管理器)处理TransactionManager-事务管理本文是以 DataSourceTransactionManager 为例来分析事务的管理实现getTransaction-获取事务public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { // 获取事务 Object transaction = doGetTransaction(); if (definition == null) { // 若 definition == null 则采用默认的事务定义 definition = new DefaultTransactionDefinition(); } // 判断当前上下文是否开启过事务 if (isExistingTransaction(transaction)) { // 当前上下文开启过事务 // 如果当前方法匹配的事务传播性为 PROPAGATION_NEVER 说明不需要事务则抛出异常 if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { throw new IllegalTransactionStateException(“Transaction propagation ’never’ but existing transaction found”); } // 如果当前方法匹配的事务传播性为 PROPAGATION_NOT_SUPPORTED 说明该方法不应该运行在事务中,则将当前事务挂起 if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { // 将当前事务挂起 Object suspendedResources = suspend(transaction); boolean newSynchronization = (this.transactionSynchronization == SYNCHRONIZATION_ALWAYS); // 返回的事务状态为 不需要事务 return newTransactionStatus(null, false, newSynchronization, definition.isReadOnly(), debugEnabled, suspendedResources); } // 如果当前方法匹配的事务传播性为 PROPAGATION_REQUIRES_NEW 表示当前方法必须运行在它自己的事务中;将已存在的事务挂起,重新开启事务 if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { if (debugEnabled) { logger.debug(“Creating new transaction, suspending current one”); } // 挂起当前事务 Object suspendedResources = suspend(transaction); // 重新开启个事务 doBegin(transaction, definition); boolean newSynchronization = (this.transactionSynchronization != SYNCHRONIZATION_NEVER); // 返回的事务状态为 新建事务 return newTransactionStatus(transaction, true, newSynchronization, definition.isReadOnly(), debugEnabled, suspendedResources); } else { boolean newSynchronization = (this.transactionSynchronization != SYNCHRONIZATION_NEVER); // 其他的传播行为 表示在已存在的事务中执行 return newTransactionStatus(transaction, false, newSynchronization, definition.isReadOnly(), debugEnabled, null); } } if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException(“Invalid transaction timeout”, definition.getTimeout()); } // 如果传播性为 PROPAGATION_MANDATORY 说明必须在事务中执行,若当前没有事务的话则抛出异常 if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”); } // 当前上下文不存在事务 // 若传播性为 PROPAGATION_REQUIRED 或 PROPAGATION_REQUIRES_NEW 则开启新的事务执行 if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { // 开启新的 connection 并取消自动提交,将 connection 绑定当前线程 doBegin(transaction, definition); boolean newSynchronization = (this.transactionSynchronization != SYNCHRONIZATION_NEVER); return newTransactionStatus(transaction, true, newSynchronization, definition.isReadOnly(), debugEnabled, null); } else { // “empty” (-> no) transaction boolean newSynchronization = (this.transactionSynchronization == SYNCHRONIZATION_ALWAYS); // 返回事务状态为 不需要事务 return newTransactionStatus(null, false, newSynchronization, definition.isReadOnly(), debugEnabled, null); }}protected Object doGetTransaction() { // 判断当前线程是否开启过事务 if (TransactionSynchronizationManager.hasResource(this.dataSource)) { // 获取当前已存在的 connectoin holder ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource); return new DataSourceTransactionObject(holder); } else { return new DataSourceTransactionObject(); }}看到了这里,是不是突然明白 PROPAGATION (事务传播性) 是干什么的了;简单来说, PROPAGATION 就是为了告诉 Spring 当前方法需不需要事务,是在已存在的事务中执行,还是新开启事务执行;也可以认为是继承上个方法栈的事务,还是拥有自己的事务。TransactionManager 获取事务的过程实际就是通过当前方法定义的 PROPAGATION (事务传播性) 和当前上下文是否存在事务来判断是否需要事务,是否需要开启新的事务或者是使用当前已存在的事务。下面看下如何开启新的事务 doBeginprotected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; // cache to avoid repeated checks boolean debugEnabled = logger.isDebugEnabled(); // 判断 connection holder 是否为空 // 两种场景下可能为空: // 1. 上下文不存在事务的时候 // 2. 上下文已存在的事务被挂起的时候 if (txObject.getConnectionHolder() == null) { if (debugEnabled) { logger.debug(“Opening new connection for JDBC transaction”); } // 开启新的 connection Connection con = DataSourceUtils.getConnection(this.dataSource, false); txObject.setConnectionHolder(new ConnectionHolder(con)); } Connection con = txObject.getConnectionHolder().getConnection(); try { // apply read-only if (definition.isReadOnly()) { try { // 如果定义了只读,设置 connection 为只读 con.setReadOnly(true); } catch (Exception ex) { // SQLException or UnsupportedOperationException logger.warn(“Could not set JDBC connection read-only”, ex); } } // apply isolation level // 设置事务隔离级别 if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { txObject.setPreviousIsolationLevel(new Integer(con.getTransactionIsolation())); con.setTransactionIsolation(definition.getIsolationLevel()); } // 若 connection 为自动提交则取消 if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true); con.setAutoCommit(false); } // 设置超时时间 if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getConnectionHolder().setTimeoutInSeconds(definition.getTimeout()); } // 将当前 connection holder 绑定到当前上下文 TransactionSynchronizationManager.bindResource(this.dataSource, txObject.getConnectionHolder()); } catch (SQLException ex) { throw new CannotCreateTransactionException(“Could not configure connection”, ex); }}doBegin 执行开启事务的操作,在上下文不存在事务或者上下文事务被挂起的时候会新打开一个 connection, 并按照事务定义设置相关属性,譬如是否只读,取消自动提交,设置事务隔离级别,设置超时时间;最后会将 connection 绑定到当前上下文,也即当前线程。doSuspend-事务挂起protected Object doSuspend(Object transaction) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; // 将当前事务的 connection holder 置为空 txObject.setConnectionHolder(null); // 并将当前事务与上下文解绑 return TransactionSynchronizationManager.unbindResource(this.dataSource);}事务挂起既是将当前事务的连接持有者清空并与当前上下文解绑,保证后续能够重新开启事务。数据库操作针对数据库的操作,本文以 Spring 提供的 jdbcTemplate 工具类进行分析。public Object execute(final StatementCallback action) { // 若当前需要事务管理的话,那么此时获取的 connection 则是 transaction manager bind 的 connection // 这样就保证数据库操作的时候所获得的的 connection 与 事务管理的一致 Connection con = DataSourceUtils.getConnection(getDataSource()); Statement stmt = null; // 以下代码省略 此处重点关注如何获取 connection}public static Connection getConnection(DataSource ds, boolean allowSynchronization) throws CannotGetJdbcConnectionException { // 从当前上下文获取 connection holder ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(ds); if (conHolder != null) { return conHolder.getConnection(); } else { try { // 反之新打开一个 connection Connection con = ds.getConnection(); if (allowSynchronization && TransactionSynchronizationManager.isSynchronizationActive()) { logger.debug(“Registering transaction synchronization for JDBC connection”); // use same Connection for further JDBC actions within the transaction // thread object will get removed by synchronization at transaction completion conHolder = new ConnectionHolder(con); TransactionSynchronizationManager.bindResource(ds, conHolder); TransactionSynchronizationManager.registerSynchronization(new ConnectionSynchronization(conHolder, ds)); } return con; } catch (SQLException ex) { throw new CannotGetJdbcConnectionException(“Could not get JDBC connection”, ex); } }}从上述代码我们可以看到,当通过 jdbcTemplate 操作数据库时会先从当前上下文中获取 connection; 这样就保证了所获取的事务与事务拦截器的事务为同一个实例,也就是将事务交给了 Spring 来管理。commit-事务提交public final void commit(TransactionStatus status) throws TransactionException { DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; // 省略 else { try { try { triggerBeforeCommit(defStatus); triggerBeforeCompletion(defStatus); if (status.isNewTransaction()) { logger.info(“Initiating transaction commit”); // 执行事务提交 doCommit(defStatus); } } // 省略 } finally { cleanupAfterCompletion(defStatus); } }}doCommit 执行事务提交protected void doCommit(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); if (status.isDebug()) { logger.debug(“Committing JDBC transaction [” + txObject.getConnectionHolder().getConnection() + “]”); } try { // 事务提交 txObject.getConnectionHolder().getConnection().commit(); } catch (SQLException ex) { throw new TransactionSystemException(“Could not commit”, ex); }}resume-事务恢复从上文的 commit 事务提交操作发现,在完成事务提交之后,还有个后置动作 cleanupAfterCompletion, 该方法会对挂起中的事务执行恢复操作。private void cleanupAfterCompletion(DefaultTransactionStatus status) { if (status.isNewSynchronization()) { TransactionSynchronizationManager.clearSynchronization(); } if (status.isNewTransaction()) { doCleanupAfterCompletion(status.getTransaction()); } // 当存在挂起的事务时,执行恢复挂起的事务 if (status.getSuspendedResources() != null) { if (status.isDebug()) { logger.debug(“Resuming suspended transaction”); } resume(status.getTransaction(), status.getSuspendedResources()); }}protected void doResume(Object transaction, Object suspendedResources) { // 将挂起的事务绑定的 connection 重新绑定到当前上下文 ConnectionHolder conHolder = (ConnectionHolder) suspendedResources; TransactionSynchronizationManager.bindResource(this.dataSource, conHolder);}事务的 resume 就是将挂起的事务重新绑定到当前上下文中。rollback-事务回滚当 TransactionInterceptor 调用目标方法执行出现异常的时候,会进行异常处理执行方法 onThrowableprivate void onThrowable(MethodInvocation invocation, TransactionAttribute txAtt, TransactionStatus status, Throwable ex) { if (txAtt.rollbackOn(ex)) { try { // 异常需要回滚 this.transactionManager.rollback(status); } catch (TransactionException tex) { throw tex; } } else { // 异常不需要回滚的话 则提交事务 this.transactionManager.commit(status); }}onThrowable 方法会通过配置判断当前异常是否需要回滚。public final void rollback(TransactionStatus status) throws TransactionException { DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; try { try { triggerBeforeCompletion(defStatus); if (status.isNewTransaction()) { // 执行事务回滚 logger.info(“Initiating transaction rollback”); doRollback(defStatus); } else if (defStatus.getTransaction() != null) { if (defStatus.isDebug()) { logger.debug(“Setting existing transaction rollback-only”); } doSetRollbackOnly(defStatus); } else { logger.info(“Should roll back transaction but cannot - no transaction available”); } } catch (TransactionException ex) { triggerAfterCompletion(defStatus, TransactionSynchronization.STATUS_UNKNOWN, ex); throw ex; } triggerAfterCompletion(defStatus, TransactionSynchronization.STATUS_ROLLED_BACK, null); } finally { cleanupAfterCompletion(defStatus); }}protected void doRollback(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); try { // 执行回滚 txObject.getConnectionHolder().getConnection().rollback(); } catch (SQLException ex) { throw new TransactionSystemException(“Could not rollback”, ex); }}小结此时我们基本明白了 Spring Transaction 的实现原理,下面对其实现做个小结:Spring Transaction 是基于 Spring AOP 的一种实现Spring Transaction 通过配置创建事务 advisor 并创建目标对象代理类目标方法执行时将会被 TransactionInterceptor 拦截TransactionInterceptor 会委派 TransactionManager 执行事务的创建,事务提交,事务回滚的动作TransactionManager 会根据当前方法配置的事务传播性及当前上下文是否存在事务来判断是否新建事务TransactionManager 当新建事务时会将事务绑定到当前上下文,以保证目标方法执行时获取的事务为同一实例TransactionManager 执行事务挂起时会将当前事务与当前上下文解除绑定关系TransactionManager 执行事务恢复时会将已挂起的事务重新与当前上下文绑定 ...

March 10, 2019 · 9 min · jiezi

动态jsonView

在从后台数据获取时,发现并没有自己想要的字段,原因是后台使用jsonView并没有包含自己想要的字段.动态jsonView一开始想重新写一个方法,使用新定义的jsonView,但是功能都一样,感觉没有必要.因为就是需要使用不同的jsonView,所以考虑能不能根据情况使用不同的jsonView返回数据.解决在stackoverflow上找到了解决方法,对此他有一段描述:On the off chance someone else wants to achieve the same thing, it actually is very simple.You can directly return aorg.springframework.http.converter.json.MappingJacksonValue instance >from your controller that contains both the object that you want to serialise and the view >class.This will be picked up by the org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#writeInternal method and the appropriate view will be used.大意是说可以通过返回MappingJacksonValue这个类的实例来解决这个问题,并且给出了实例代码:@RequestMapping(value = “/accounts/{id}”, method = GET, produces = APPLICATION_JSON_VALUE)public MappingJacksonValue getAccount(@PathVariable(“id”) String accountId, @AuthenticationPrincipal User user) { final Account account = accountService.get(accountId); final MappingJacksonValue result = new MappingJacksonValue(account); final Class<? extends View> view = accountPermissionsService.getViewForUser(user); result.setSerializationView(view); return result;}首先就是创建MappingJacksonValue类实例,在构造函数中传入要序列化的对象,之后调用setSerializationView方法传入jsonView的class对象就行了,AbstractJackson2HttpMessageConverter类会处理这个对象并根据传入jsonView来序列化对象.#最终最终代码:@GetMapping @ResponseStatus(HttpStatus.OK) private MappingJacksonValue getAll(@RequestParam(required = false, value = “isCascade”, defaultValue = “false”) boolean cascade) { List<College> collegeList = collegeService.getAllCollege(); MappingJacksonValue result = new MappingJacksonValue(collegeList); if (cascade) { result.setSerializationView(CollegeJsonView.getCascadeMessage.class); } else { result.setSerializationView(CollegeJsonView.getAll.class); } return result; }根据isCascade参数来判断是否返回自定义的josnView数据。 ...

March 9, 2019 · 1 min · jiezi

Java抽象类和接口小记

Java抽象类和接口小记Java抽象类和接口实现了java的多态.多态是面向对象程序语言的核心,在项目开发过程中,其实很少使用抽象类,接口用得比较多,今天小记一下抽象类和接口的区别.抽象类/* * 抽象类不能被实例化 * 抽象类可以继承 * 可以定义变量 * 可以定义构造方法 * 抽象方法的abstract要显式的写出来 * 其子类必须实现抽象类的所有抽象方法 /abstract class ab extends Object{ int a = 0; public ab() { } public abstract void f();}总结:抽象类和普通类的差别是,不能被实例化,可以定义抽象方法,但子类必须实现接口/* * 可以用extends继承其它接口 * 只能定义常量,不能定义变量 * 不能定义构造方法 * 方法全是抽象方法 * 实现类必须实现其所有抽象方法 */interface In { public int a=0; public void a(); public void b();}2.1 接口的实现class InImpl implements In{ @Override public void a() { } @Override public void b() { }}总结:接口比抽象类更抽象,只能定义抽象方法,也只能定义常量,不能定义构造方法.想想也是,接口定义的是一种规范.在工业上,接口定义不就是插槽的基本参数和技术规范吗?因此接口称之为接口果然名副其实.

March 7, 2019 · 1 min · jiezi

设计模式超级简单的解释

推荐阅读design-patterns-for-humans 中文版MongoDB 资源、库、工具、应用程序精选列表中文版有哪些鲜为人知,但是很有意思的网站?一份攻城狮笔记每天搜集 Github 上优秀的项目一些有趣的民间故事超好用的谷歌浏览器、Sublime Text、Phpstorm、油猴插件合集*设计模式超简单的解释!(本项目从 design-patterns-for-humans fork)介绍设计模式是反复出现问题的解决方案; 如何解决某些问题的指导方针。它们不是可以插入应用程序并等待神奇发生的类,包或库。相反,这些是如何在某些情况下解决某些问题的指导原则。设计模式是反复出现问题的解决方案; 如何解决某些问题的指导方针维基百科将它们描述为在软件工程中,软件设计模式是软件设计中给定上下文中常见问题的通用可重用解决方案。它不是可以直接转换为源代码或机器代码的完成设计。它是如何解决可在许多不同情况下使用的问题的描述或模板。⚠️注意设计模式不是解决所有问题的灵丹妙药。不要试图强迫他们; 如果这样做的话,应该发生坏事。请记住,设计模式是问题的解决方案,而不是解决问题的解决方案;所以不要过分思考。如果以正确的方式在正确的地方使用,他们可以证明是救世主; 否则他们可能会导致代码混乱。另请注意,下面的代码示例是PHP-7,但是这不应该阻止你因为概念是相同的。设计模式的类型创建型结构型行为型创建型设计模式简单来说创建模式专注于如何实例化对象或相关对象组。维基百科说在软件工程中,创建设计模式是处理对象创建机制的设计模式,试图以适合于该情况的方式创建对象。对象创建的基本形式可能导致设计问题或增加设计的复杂性。创建设计模式通过某种方式控制此对象创建来解决此问题。简单工厂模式(Simple Factory)工厂方法模式(Factory Method)抽象工厂模式(Abstract Factory)构建器模式原型模式(Prototype)单例模式(Singleton)????简单工厂模式(Simple Factory)现实世界的例子考虑一下,你正在建房子,你需要门。你可以穿上你的木匠衣服,带上一些木头,胶水,钉子和建造门所需的所有工具,然后开始在你的房子里建造它,或者你可以简单地打电话给工厂并把内置的门送到你这里不需要了解关于制门的任何信息或处理制作它所带来的混乱。简单来说简单工厂只是为客户端生成一个实例,而不会向客户端公开任何实例化逻辑维基百科说在面向对象编程(OOP)中,工厂是用于创建其他对象的对象 - 正式工厂是一种函数或方法,它从一些方法调用返回变化的原型或类的对象,这被假定为“新”。程序化示例首先,我们有一个门界面和实现interface Door{ public function getWidth(): float; public function getHeight(): float;}class WoodenDoor implements Door{ protected $width; protected $height; public function __construct(float $width, float $height) { $this->width = $width; $this->height = $height; } public function getWidth(): float { return $this->width; } public function getHeight(): float { return $this->height; }}然后,我们有我们的门工厂,门,并返回它class DoorFactory{ public static function makeDoor($width, $height): Door { return new WoodenDoor($width, $height); }}然后它可以用作// Make me a door of 100x200$door = DoorFactory::makeDoor(100, 200);echo ‘Width: ’ . $door->getWidth();echo ‘Height: ’ . $door->getHeight();// Make me a door of 50x100$door2 = DoorFactory::makeDoor(50, 100);什么时候用?当创建一个对象不仅仅是一些分配而且涉及一些逻辑时,将它放在专用工厂中而不是在任何地方重复相同的代码是有意义的。????工厂方法模式(Factory Method)现实世界的例子考虑招聘经理的情况。一个人不可能对每个职位进行面试。根据职位空缺,她必须决定并将面试步骤委托给不同的人。简单来说它提供了一种将实例化逻辑委托给子类的方法。维基百科说在基于类的编程中,工厂方法模式是一种创建模式,它使用工厂方法来处理创建对象的问题,而无需指定将要创建的对象的确切类。这是通过调用工厂方法来创建对象来完成的 - 在接口中指定并由子类实现,或者在基类中实现并可选地由派生类覆盖 - 而不是通过调用构造函数。程序化示例以上面的招聘经理为例。首先,我们有一个访谈者界面和一些实现interface Interviewer{ public function askQuestions();}class Developer implements Interviewer{ public function askQuestions() { echo ‘Asking about design patterns!’; }}class CommunityExecutive implements Interviewer{ public function askQuestions() { echo ‘Asking about community building’; }}现在让我们创造我们的 HiringManagerabstract class HiringManager{ // Factory method abstract protected function makeInterviewer(): Interviewer; public function takeInterview() { $interviewer = $this->makeInterviewer(); $interviewer->askQuestions(); }}现在任何孩子都可以延长并提供所需的面试官class DevelopmentManager extends HiringManager{ protected function makeInterviewer(): Interviewer { return new Developer(); }}class MarketingManager extends HiringManager{ protected function makeInterviewer(): Interviewer { return new CommunityExecutive(); }}然后它可以用作$devManager = new DevelopmentManager();$devManager->takeInterview(); // Output: Asking about design patterns$marketingManager = new MarketingManager();$marketingManager->takeInterview(); // Output: Asking about community building.什么时候用?在类中有一些通用处理但在运行时动态决定所需的子类时很有用。换句话说,当客户端不知道它可能需要什么样的子类时。????抽象工厂模式(Abstract Factory)现实世界的例子从Simple Factory扩展我们的门例子。根据您的需求,您可以从木门店,铁门的铁门或相关商店的PVC门获得木门。另外,你可能需要一个有不同种类特色的家伙来安装门,例如木门木匠,铁门焊机等。你可以看到门之间存在依赖关系,木门需要木匠,铁门需要焊工等简单来说工厂工厂; 将个人但相关/依赖工厂分组在一起而不指定其具体类别的工厂。维基百科说抽象工厂模式提供了一种封装一组具有共同主题但没有指定其具体类的单个工厂的方法程序化示例翻译上面的门例子。首先,我们有我们的Door界面和一些实现interface Door{ public function getDescription();}class WoodenDoor implements Door{ public function getDescription() { echo ‘I am a wooden door’; }}class IronDoor implements Door{ public function getDescription() { echo ‘I am an iron door’; }}然后我们为每种门类型都配备了一些装配专家interface DoorFittingExpert{ public function getDescription();}class Welder implements DoorFittingExpert{ public function getDescription() { echo ‘I can only fit iron doors’; }}class Carpenter implements DoorFittingExpert{ public function getDescription() { echo ‘I can only fit wooden doors’; }}现在我们有抽象工厂,让我们制作相关对象的家庭,即木门工厂将创建一个木门和木门配件专家和铁门工厂将创建一个铁门和铁门配件专家interface DoorFactory{ public function makeDoor(): Door; public function makeFittingExpert(): DoorFittingExpert;}// Wooden factory to return carpenter and wooden doorclass WoodenDoorFactory implements DoorFactory{ public function makeDoor(): Door { return new WoodenDoor(); } public function makeFittingExpert(): DoorFittingExpert { return new Carpenter(); }}// Iron door factory to get iron door and the relevant fitting expertclass IronDoorFactory implements DoorFactory{ public function makeDoor(): Door { return new IronDoor(); } public function makeFittingExpert(): DoorFittingExpert { return new Welder(); }}然后它可以用作$woodenFactory = new WoodenDoorFactory();$door = $woodenFactory->makeDoor();$expert = $woodenFactory->makeFittingExpert();$door->getDescription(); // Output: I am a wooden door$expert->getDescription(); // Output: I can only fit wooden doors// Same for Iron Factory$ironFactory = new IronDoorFactory();$door = $ironFactory->makeDoor();$expert = $ironFactory->makeFittingExpert();$door->getDescription(); // Output: I am an iron door$expert->getDescription(); // Output: I can only fit iron doors正如你所看到的木门工厂的封装carpenter和wooden door还铁门厂已封装的iron door和welder。因此,它帮助我们确保对于每个创建的门,我们没有得到错误的拟合专家。什么时候用?当存在相互关联的依赖关系时,涉及非简单的创建逻辑????构建器模式现实世界的例子想象一下,你在Hardee’s,你订购了一个特定的交易,让我们说,“Big Hardee”,他们毫无_疑问地_把它交给你了; 这是简单工厂的例子。但有些情况下,创建逻辑可能涉及更多步骤。例如,你想要一个定制的地铁交易,你有多种选择如何制作你的汉堡,例如你想要什么面包?你想要什么类型的酱汁?你想要什么奶酪?在这种情况下,建筑商模式得以拯救。简单来说允许您创建不同风格的对象,同时避免构造函数污染。当有几种风格的物体时很有用。或者在创建对象时涉及很多步骤。维基百科说构建器模式是对象创建软件设计模式,其目的是找到伸缩构造器反模式的解决方案。话虽如此,让我补充说一下伸缩构造函数反模式是什么。在某一点或另一点,我们都看到了如下构造函数:public function __construct($size, $cheese = true, $pepperoni = true, $tomato = false, $lettuce = true){}如你看到的; 构造函数参数的数量很快就会失控,并且可能难以理解参数的排列。此外,如果您希望将来添加更多选项,此参数列表可能会继续增长。这被称为伸缩构造器反模式。程序化示例理智的替代方案是使用构建器模式。首先,我们要制作汉堡class Burger{ protected $size; protected $cheese = false; protected $pepperoni = false; protected $lettuce = false; protected $tomato = false; public function __construct(BurgerBuilder $builder) { $this->size = $builder->size; $this->cheese = $builder->cheese; $this->pepperoni = $builder->pepperoni; $this->lettuce = $builder->lettuce; $this->tomato = $builder->tomato; }}然后我们有了建设者class BurgerBuilder{ public $size; public $cheese = false; public $pepperoni = false; public $lettuce = false; public $tomato = false; public function __construct(int $size) { $this->size = $size; } public function addPepperoni() { $this->pepperoni = true; return $this; } public function addLettuce() { $this->lettuce = true; return $this; } public function addCheese() { $this->cheese = true; return $this; } public function addTomato() { $this->tomato = true; return $this; } public function build(): Burger { return new Burger($this); }}然后它可以用作:$burger = (new BurgerBuilder(14)) ->addPepperoni() ->addLettuce() ->addTomato() ->build();什么时候用?当可能存在几种类型的对象并避免构造函数伸缩时。与工厂模式的主要区别在于:当创建是一步过程时,将使用工厂模式,而当创建是多步骤过程时,将使用构建器模式。????原型模式(Prototype)现实世界的例子记得多莉?被克隆的羊!让我们不详细介绍,但关键点在于它完全是关于克隆的简单来说通过克隆基于现有对象创建对象。维基百科说原型模式是软件开发中的创新设计模式。当要创建的对象类型由原型实例确定时使用它,该实例被克隆以生成新对象。简而言之,它允许您创建现有对象的副本并根据需要进行修改,而不是从头开始创建对象并进行设置。程序化示例在PHP中,它可以很容易地使用 cloneclass Sheep{ protected $name; protected $category; public function __construct(string $name, string $category = ‘Mountain Sheep’) { $this->name = $name; $this->category = $category; } public function setName(string $name) { $this->name = $name; } public function getName() { return $this->name; } public function setCategory(string $category) { $this->category = $category; } public function getCategory() { return $this->category; }}然后它可以像下面一样克隆$original = new Sheep(‘Jolly’);echo $original->getName(); // Jollyecho $original->getCategory(); // Mountain Sheep// Clone and modify what is required$cloned = clone $original;$cloned->setName(‘Dolly’);echo $cloned->getName(); // Dollyecho $cloned->getCategory(); // Mountain sheep您也可以使用魔术方法__clone来修改克隆行为。什么时候用?当需要一个与现有对象类似的对象时,或者与克隆相比,创建的成本会很高。????单例模式(Singleton)现实世界的例子一次只能有一个国家的总统。无论何时打电话,都必须将同一位总统付诸行动。这里的总统是单身人士。简单来说确保只创建特定类的一个对象。维基百科说在软件工程中,单例模式是一种软件设计模式,它将类的实例化限制为一个对象。当需要一个对象来协调整个系统的操作时,这非常有用。单例模式实际上被认为是反模式,应该避免过度使用它。它不一定是坏的,可能有一些有效的用例,但应谨慎使用,因为它在您的应用程序中引入了一个全局状态,并且在一个地方更改它可能会影响其他区域,并且它可能变得非常难以调试。关于它们的另一个坏处是它使你的代码紧密耦合加上嘲弄单例可能很困难。程序化示例要创建单例,请将构造函数设为私有,禁用克隆,禁用扩展并创建静态变量以容纳实例final class President{ private static $instance; private function __construct() { // Hide the constructor } public static function getInstance(): President { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } private function __clone() { // Disable cloning } private function __wakeup() { // Disable unserialize }}然后才能使用$president1 = President::getInstance();$president2 = President::getInstance();var_dump($president1 === $president2); // true结构型设计模式简单来说结构模式主要涉及对象组成,或者换句话说,实体如何相互使用。或者另一种解释是,它们有助于回答“如何构建软件组件?”维基百科说在软件工程中,结构设计模式是通过识别实现实体之间关系的简单方法来简化设计的设计模式。适配器模式(Adapter)桥梁模式(Bridge)组合模式(Composite)装饰模式(Decorator)门面模式(Facade)享元模式(Flyweight)代理模式(Proxy)????适配器模式(Adapter)现实世界的例子请注意,您的存储卡中有一些照片,需要将它们传输到计算机上。为了传输它们,您需要某种与您的计算机端口兼容的适配器,以便您可以将存储卡连接到您的计算机。在这种情况下,读卡器是适配器。另一个例子是着名的电源适配器; 三脚插头不能连接到双管插座,需要使用电源适配器,使其与双叉插座兼容。另一个例子是翻译人员将一个人所说的话翻译成另一个人简单来说适配器模式允许您在适配器中包装其他不兼容的对象,以使其与另一个类兼容。维基百科说在软件工程中,适配器模式是一种软件设计模式,它允许将现有类的接口用作另一个接口。它通常用于使现有类与其他类一起工作而无需修改其源代码。程序化示例考虑一个有猎人的游戏,他猎杀狮子。首先,我们有一个Lion所有类型的狮子必须实现的接口interface Lion{ public function roar();}class AfricanLion implements Lion{ public function roar() { }}class AsianLion implements Lion{ public function roar() { }}猎人期望任何Lion接口的实现都可以进行搜索。class Hunter{ public function hunt(Lion $lion) { $lion->roar(); }}现在让我们说我们必须WildDog在我们的游戏中添加一个,以便猎人也可以追捕它。但我们不能直接这样做,因为狗有不同的界面。为了使它与我们的猎人兼容,我们将不得不创建一个兼容的适配器// This needs to be added to the gameclass WildDog{ public function bark() { }}// Adapter around wild dog to make it compatible with our gameclass WildDogAdapter implements Lion{ protected $dog; public function __construct(WildDog $dog) { $this->dog = $dog; } public function roar() { $this->dog->bark(); }}而现在WildDog可以在我们的游戏中使用WildDogAdapter。$wildDog = new WildDog();$wildDogAdapter = new WildDogAdapter($wildDog);$hunter = new Hunter();$hunter->hunt($wildDogAdapter);????桥梁模式(Bridge)现实世界的例子考虑您有一个包含不同页面的网站,您应该允许用户更改主题。你会怎么做?为每个主题创建每个页面的多个副本,或者您只是创建单独的主题并根据用户的首选项加载它们?桥模式允许你做第二个ie用简单的话说桥模式是关于优先于继承的组合。实现细节从层次结构推送到具有单独层次结构的另一个对象。维基百科说桥接模式是软件工程中使用的设计模式,旨在“将抽象与其实现分离,以便两者可以独立变化”程序化示例从上面翻译我们的WebPage示例。这里我们有WebPage层次结构interface WebPage{ public function __construct(Theme $theme); public function getContent();}class About implements WebPage{ protected $theme; public function __construct(Theme $theme) { $this->theme = $theme; } public function getContent() { return “About page in " . $this->theme->getColor(); }}class Careers implements WebPage{ protected $theme; public function __construct(Theme $theme) { $this->theme = $theme; } public function getContent() { return “Careers page in " . $this->theme->getColor(); }}和单独的主题层次结构interface Theme{ public function getColor();}class DarkTheme implements Theme{ public function getColor() { return ‘Dark Black’; }}class LightTheme implements Theme{ public function getColor() { return ‘Off white’; }}class AquaTheme implements Theme{ public function getColor() { return ‘Light blue’; }}而且这两个层次结构$darkTheme = new DarkTheme();$about = new About($darkTheme);$careers = new Careers($darkTheme);echo $about->getContent(); // “About page in Dark Black”;echo $careers->getContent(); // “Careers page in Dark Black”;????组合模式(Composite)现实世界的例子每个组织都由员工组成。每个员工都有相同的功能,即有工资,有一些责任,可能会或可能不会向某人报告,可能会或可能不会有一些下属等。简单来说复合模式允许客户以统一的方式处理单个对象。维基百科说在软件工程中,复合模式是分区设计模式。复合模式描述了一组对象的处理方式与对象的单个实例相同。复合的意图是将对象“组合”成树结构以表示部分整体层次结构。通过实现复合模式,客户可以统一处理单个对象和组合。程序化示例以上面的员工为例。这里我们有不同的员工类型interface Employee{ public function __construct(string $name, float $salary); public function getName(): string; public function setSalary(float $salary); public function getSalary(): float; public function getRoles(): array;}class Developer implements Employee{ protected $salary; protected $name; protected $roles; public function __construct(string $name, float $salary) { $this->name = $name; $this->salary = $salary; } public function getName(): string { return $this->name; } public function setSalary(float $salary) { $this->salary = $salary; } public function getSalary(): float { return $this->salary; } public function getRoles(): array { return $this->roles; }}class Designer implements Employee{ protected $salary; protected $name; protected $roles; public function __construct(string $name, float $salary) { $this->name = $name; $this->salary = $salary; } public function getName(): string { return $this->name; } public function setSalary(float $salary) { $this->salary = $salary; } public function getSalary(): float { return $this->salary; } public function getRoles(): array { return $this->roles; }}然后我们有一个由几种不同类型的员工组成的组织class Organization{ protected $employees; public function addEmployee(Employee $employee) { $this->employees[] = $employee; } public function getNetSalaries(): float { $netSalary = 0; foreach ($this->employees as $employee) { $netSalary += $employee->getSalary(); } return $netSalary; }}然后它可以用作// Prepare the employees$john = new Developer(‘John Doe’, 12000);$jane = new Designer(‘Jane Doe’, 15000);// Add them to organization$organization = new Organization();$organization->addEmployee($john);$organization->addEmployee($jane);echo “Net salaries: " . $organization->getNetSalaries(); // Net Salaries: 27000☕装饰模式(Decorator)现实世界的例子想象一下,您经营一家提供多种服务的汽车服务店。现在你如何计算收费账单?您选择一项服务并动态地向其添加所提供服务的价格,直到您获得最终成本。这里的每种服务都是装饰者。简单来说Decorator模式允许您通过将对象包装在装饰器类的对象中来动态更改对象在运行时的行为。维基百科说在面向对象的编程中,装饰器模式是一种设计模式,它允许将行为静态或动态地添加到单个对象,而不会影响同一类中其他对象的行为。装饰器模式通常用于遵守单一责任原则,因为它允许在具有独特关注区域的类之间划分功能。程序化示例让我们以咖啡为例。首先,我们有一个简单的咖啡实现咖啡界面interface Coffee{ public function getCost(); public function getDescription();}class SimpleCoffee implements Coffee{ public function getCost() { return 10; } public function getDescription() { return ‘Simple coffee’; }}我们希望使代码可扩展,以允许选项在需要时修改它。让我们做一些附加组件(装饰器)class MilkCoffee implements Coffee{ protected $coffee; public function __construct(Coffee $coffee) { $this->coffee = $coffee; } public function getCost() { return $this->coffee->getCost() + 2; } public function getDescription() { return $this->coffee->getDescription() . ‘, milk’; }}class WhipCoffee implements Coffee{ protected $coffee; public function __construct(Coffee $coffee) { $this->coffee = $coffee; } public function getCost() { return $this->coffee->getCost() + 5; } public function getDescription() { return $this->coffee->getDescription() . ‘, whip’; }}class VanillaCoffee implements Coffee{ protected $coffee; public function __construct(Coffee $coffee) { $this->coffee = $coffee; } public function getCost() { return $this->coffee->getCost() + 3; } public function getDescription() { return $this->coffee->getDescription() . ‘, vanilla’; }}让我们现在喝杯咖啡$someCoffee = new SimpleCoffee();echo $someCoffee->getCost(); // 10echo $someCoffee->getDescription(); // Simple Coffee$someCoffee = new MilkCoffee($someCoffee);echo $someCoffee->getCost(); // 12echo $someCoffee->getDescription(); // Simple Coffee, milk$someCoffee = new WhipCoffee($someCoffee);echo $someCoffee->getCost(); // 17echo $someCoffee->getDescription(); // Simple Coffee, milk, whip$someCoffee = new VanillaCoffee($someCoffee);echo $someCoffee->getCost(); // 20echo $someCoffee->getDescription(); // Simple Coffee, milk, whip, vanilla????门面模式(Facade)现实世界的例子你怎么打开电脑?“按下电源按钮”你说!这就是你所相信的,因为你正在使用计算机在外部提供的简单界面,在内部它必须做很多事情来实现它。这个复杂子系统的简单接口是一个外观。简单来说Facade模式为复杂的子系统提供了简化的界面。维基百科说外观是一个对象,它为更大的代码体提供了简化的接口,例如类库。程序化示例从上面看我们的计算机示例。这里我们有电脑课class Computer{ public function getElectricShock() { echo “Ouch!”; } public function makeSound() { echo “Beep beep!”; } public function showLoadingScreen() { echo “Loading..”; } public function bam() { echo “Ready to be used!”; } public function closeEverything() { echo “Bup bup bup buzzzz!”; } public function sooth() { echo “Zzzzz”; } public function pullCurrent() { echo “Haaah!”; }}在这里,我们有门面class ComputerFacade{ protected $computer; public function __construct(Computer $computer) { $this->computer = $computer; } public function turnOn() { $this->computer->getElectricShock(); $this->computer->makeSound(); $this->computer->showLoadingScreen(); $this->computer->bam(); } public function turnOff() { $this->computer->closeEverything(); $this->computer->pullCurrent(); $this->computer->sooth(); }}现在使用立面$computer = new ComputerFacade(new Computer());$computer->turnOn(); // Ouch! Beep beep! Loading.. Ready to be used!$computer->turnOff(); // Bup bup buzzz! Haah! Zzzzz????享元模式(Flyweight)现实世界的例子你有没有从一些摊位买到新鲜的茶?他们经常制作你需要的不止一个杯子,并为其他任何客户保存其余的,以节省资源,例如天然气等.Flyweight模式就是那个即共享。简单来说它用于通过尽可能多地与类似对象共享来最小化内存使用或计算开销。维基百科说在计算机编程中,flyweight是一种软件设计模式。flyweight是一个通过与其他类似对象共享尽可能多的数据来最小化内存使用的对象; 当简单的重复表示将使用不可接受的内存量时,它是一种大量使用对象的方法。程序化的例子从上面翻译我们的茶例子。首先,我们有茶类和茶具// Anything that will be cached is flyweight.// Types of tea here will be flyweights.class KarakTea{}// Acts as a factory and saves the teaclass TeaMaker{ protected $availableTea = []; public function make($preference) { if (empty($this->availableTea[$preference])) { $this->availableTea[$preference] = new KarakTea(); } return $this->availableTea[$preference]; }}然后我们有TeaShop接受订单并为他们服务class TeaShop{ protected $orders; protected $teaMaker; public function __construct(TeaMaker $teaMaker) { $this->teaMaker = $teaMaker; } public function takeOrder(string $teaType, int $table) { $this->orders[$table] = $this->teaMaker->make($teaType); } public function serve() { foreach ($this->orders as $table => $tea) { echo “Serving tea to table# " . $table; } }}它可以如下使用$teaMaker = new TeaMaker();$shop = new TeaShop($teaMaker);$shop->takeOrder(’less sugar’, 1);$shop->takeOrder(‘more milk’, 2);$shop->takeOrder(‘without sugar’, 5);$shop->serve();// Serving tea to table# 1// Serving tea to table# 2// Serving tea to table# 5????代理模式(Proxy)现实世界的例子你有没有用过门禁卡进门?打开该门有多种选择,即可以使用门禁卡或按下绕过安检的按钮打开。门的主要功能是打开,但在它上面添加了一个代理来添加一些功能。让我用下面的代码示例更好地解释它。简单来说使用代理模式,类表示另一个类的功能。维基百科说代理以其最一般的形式,是一个充当其他东西的接口的类。代理是一个包装器或代理对象,客户端正在调用它来访问幕后的真实服务对象。使用代理可以简单地转发到真实对象,或者可以提供额外的逻辑。在代理中,可以提供额外的功能,例如当对真实对象的操作是资源密集时的高速缓存,或者在调用对象的操作之前检查先决条件。程序化示例从上面看我们的安全门示例。首先我们有门界面和门的实现interface Door{ public function open(); public function close();}class LabDoor implements Door{ public function open() { echo “Opening lab door”; } public function close() { echo “Closing the lab door”; }}然后我们有一个代理来保护我们想要的任何门class SecuredDoor{ protected $door; public function __construct(Door $door) { $this->door = $door; } public function open($password) { if ($this->authenticate($password)) { $this->door->open(); } else { echo “Big no! It ain’t possible.”; } } public function authenticate($password) { return $password === ‘$ecr@t’; } public function close() { $this->door->close(); }}以下是它的使用方法$door = new SecuredDoor(new LabDoor());$door->open(‘invalid’); // Big no! It ain’t possible.$door->open(’$ecr@t’); // Opening lab door$door->close(); // Closing lab door另一个例子是某种数据映射器实现。例如,我最近使用这种模式为MongoDB制作了一个ODM(对象数据映射器),我在使用魔术方法的同时围绕mongo类编写了一个代理__call()。所有方法调用被代理到原始蒙戈类和结果检索到的返回,因为它是但在的情况下,find或findOne数据被映射到所需的类对象和对象返回代替Cursor。行为型设计模式简单来说它关注对象之间的职责分配。它们与结构模式的不同之处在于它们不仅指定了结构,还概述了它们之间的消息传递/通信模式。或者换句话说,他们协助回答“如何在软件组件中运行行为?”维基百科说在软件工程中,行为设计模式是识别对象之间的共同通信模式并实现这些模式的设计模式。通过这样做,这些模式增加了执行该通信的灵活性。责任链模式(Chain Of Responsibilities)命令行模式(Command)迭代器模式(Iterator)中介者模式(Mediator)备忘录模式(Memento)观察者模式(Observer)访问者模式(Visitor)策略模式(Strategy)状态模式(State)模板方法模式(Template Method)????责任链模式(Chain Of Responsibilities)现实世界的例子例如,你有三种付款方式(A,B和C)安装在您的帐户; 每个都有不同的数量。A有100美元,B具有300美元和C具有1000美元,以及支付偏好被选择作为A再B然后C。你试着购买价值210美元的东西。使用责任链,首先A会检查帐户是否可以进行购买,如果是,则进行购买并且链条将被破坏。如果没有,请求将继续进行帐户B检查金额,如果是链将被破坏,否则请求将继续转发,直到找到合适的处理程序。在这里A,B和C 是链条的链接,整个现象是责任链。简单来说它有助于构建一系列对象。请求从一端进入并继续从一个对象到另一个对象,直到找到合适的处理程序。维基百科说在面向对象的设计中,责任链模式是一种由命令对象源和一系列处理对象组成的设计模式。每个处理对象都包含定义它可以处理的命令对象类型的逻辑; 其余的传递给链中的下一个处理对象。程序化示例翻译上面的帐户示例。首先,我们有一个基本帐户,其中包含将帐户链接在一起的逻辑和一些帐户abstract class Account{ protected $successor; protected $balance; public function setNext(Account $account) { $this->successor = $account; } public function pay(float $amountToPay) { if ($this->canPay($amountToPay)) { echo sprintf(‘Paid %s using %s’ . PHP_EOL, $amountToPay, get_called_class()); } elseif ($this->successor) { echo sprintf(‘Cannot pay using %s. Proceeding ..’ . PHP_EOL, get_called_class()); $this->successor->pay($amountToPay); } else { throw new Exception(‘None of the accounts have enough balance’); } } public function canPay($amount): bool { return $this->balance >= $amount; }}class Bank extends Account{ protected $balance; public function __construct(float $balance) { $this->balance = $balance; }}class Paypal extends Account{ protected $balance; public function __construct(float $balance) { $this->balance = $balance; }}class Bitcoin extends Account{ protected $balance; public function __construct(float $balance) { $this->balance = $balance; }}现在让我们使用上面定义的链接准备链(即Bank,Paypal,Bitcoin)// Let’s prepare a chain like below// $bank->$paypal->$bitcoin//// First priority bank// If bank can’t pay then paypal// If paypal can’t pay then bit coin$bank = new Bank(100); // Bank with balance 100$paypal = new Paypal(200); // Paypal with balance 200$bitcoin = new Bitcoin(300); // Bitcoin with balance 300$bank->setNext($paypal);$paypal->setNext($bitcoin);// Let’s try to pay using the first priority i.e. bank$bank->pay(259);// Output will be// ==============// Cannot pay using bank. Proceeding ..// Cannot pay using paypal. Proceeding ..:// Paid 259 using Bitcoin!????命令行模式(Command)现实世界的例子一个通用的例子是你在餐厅点餐。您(即Client)要求服务员(即Invoker)携带一些食物(即Command),服务员只是将请求转发给主厨(即Receiver),该主厨知道什么以及如何烹饪。另一个例子是你(即)使用遥控器()Client打开(即Command)电视(即)。ReceiverInvoker简单来说允许您将操作封装在对象中。这种模式背后的关键思想是提供将客户端与接收器分离的方法。维基百科说在面向对象的编程中,命令模式是行为设计模式,其中对象用于封装执行动作或稍后触发事件所需的所有信息。此信息包括方法名称,拥有该方法的对象以及方法参数的值。程序化示例首先,我们有接收器,它可以执行每个可以执行的操作// Receiverclass Bulb{ public function turnOn() { echo “Bulb has been lit”; } public function turnOff() { echo “Darkness!”; }}然后我们有一个接口,每个命令将实现,然后我们有一组命令interface Command{ public function execute(); public function undo(); public function redo();}// Commandclass TurnOn implements Command{ protected $bulb; public function __construct(Bulb $bulb) { $this->bulb = $bulb; } public function execute() { $this->bulb->turnOn(); } public function undo() { $this->bulb->turnOff(); } public function redo() { $this->execute(); }}class TurnOff implements Command{ protected $bulb; public function __construct(Bulb $bulb) { $this->bulb = $bulb; } public function execute() { $this->bulb->turnOff(); } public function undo() { $this->bulb->turnOn(); } public function redo() { $this->execute(); }}然后我们Invoker与客户端进行交互以处理任何命令// Invokerclass RemoteControl{ public function submit(Command $command) { $command->execute(); }}最后,让我们看看我们如何在客户端使用它$bulb = new Bulb();$turnOn = new TurnOn($bulb);$turnOff = new TurnOff($bulb);$remote = new RemoteControl();$remote->submit($turnOn); // Bulb has been lit!$remote->submit($turnOff); // Darkness!命令模式还可用于实现基于事务的系统。在执行命令时,一直保持命令历史记录的位置。如果成功执行了最后一个命令,那么所有好处都只是遍历历史记录并继续执行undo所有已执行的命令。➿迭代器模式(Iterator)现实世界的例子旧的无线电设备将是迭代器的一个很好的例子,用户可以从某个频道开始,然后使用下一个或上一个按钮来浏览相应的频道。或者以MP3播放器或电视机为例,您可以按下下一个和上一个按钮来浏览连续的频道,换句话说,它们都提供了一个界面来迭代各自的频道,歌曲或电台。简单来说它提供了一种访问对象元素而不暴露底层表示的方法。维基百科说在面向对象的编程中,迭代器模式是一种设计模式,其中迭代器用于遍历容器并访问容器的元素。迭代器模式将算法与容器分离; 在某些情况下,算法必然是特定于容器的,因此不能解耦。程序化的例子在PHP中,使用SPL(标准PHP库)很容易实现。从上面翻译我们的广播电台示例。首先,我们有RadioStationclass RadioStation{ protected $frequency; public function __construct(float $frequency) { $this->frequency = $frequency; } public function getFrequency(): float { return $this->frequency; }}然后我们有了迭代器use Countable;use Iterator;class StationList implements Countable, Iterator{ /* @var RadioStation[] $stations / protected $stations = []; /* @var int $counter */ protected $counter; public function addStation(RadioStation $station) { $this->stations[] = $station; } public function removeStation(RadioStation $toRemove) { $toRemoveFrequency = $toRemove->getFrequency(); $this->stations = array_filter($this->stations, function (RadioStation $station) use ($toRemoveFrequency) { return $station->getFrequency() !== $toRemoveFrequency; }); } public function count(): int { return count($this->stations); } public function current(): RadioStation { return $this->stations[$this->counter]; } public function key() { return $this->counter; } public function next() { $this->counter++; } public function rewind() { $this->counter = 0; } public function valid(): bool { return isset($this->stations[$this->counter]); }}然后它可以用作$stationList = new StationList();$stationList->addStation(new RadioStation(89));$stationList->addStation(new RadioStation(101));$stationList->addStation(new RadioStation(102));$stationList->addStation(new RadioStation(103.2));foreach($stationList as $station) { echo $station->getFrequency() . PHP_EOL;}$stationList->removeStation(new RadioStation(89)); // Will remove station 89????中介者模式(Mediator)现实世界的例子一个典型的例子就是当你在手机上与某人交谈时,有一个网络提供商坐在你和他们之间,你的对话通过它而不是直接发送。在这种情况下,网络提供商是中介。简单来说Mediator模式添加第三方对象(称为mediator)来控制两个对象(称为同事)之间的交互。它有助于减少彼此通信的类之间的耦合。因为现在他们不需要了解彼此的实施。维基百科说在软件工程中,中介模式定义了一个对象,该对象封装了一组对象的交互方式。由于它可以改变程序的运行行为,因此这种模式被认为是一种行为模式。程序化示例这是聊天室(即中介)与用户(即同事)相互发送消息的最简单示例。首先,我们有调解员即聊天室interface ChatRoomMediator { public function showMessage(User $user, string $message);}// Mediatorclass ChatRoom implements ChatRoomMediator{ public function showMessage(User $user, string $message) { $time = date(‘M d, y H:i’); $sender = $user->getName(); echo $time . ‘[’ . $sender . ‘]:’ . $message; }}然后我们有我们的用户,即同事class User { protected $name; protected $chatMediator; public function __construct(string $name, ChatRoomMediator $chatMediator) { $this->name = $name; $this->chatMediator = $chatMediator; } public function getName() { return $this->name; } public function send($message) { $this->chatMediator->showMessage($this, $message); }}和用法$mediator = new ChatRoom();$john = new User(‘John Doe’, $mediator);$jane = new User(‘Jane Doe’, $mediator);$john->send(‘Hi there!’);$jane->send(‘Hey!’);// Output will be// Feb 14, 10:58 [John]: Hi there!// Feb 14, 10:58 [Jane]: Hey!????备忘录模式(Memento)现实世界的例子以计算器(即发起者)为例,无论何时执行某些计算,最后的计算都会保存在内存中(即纪念品),以便您可以回到它并使用某些操作按钮(即看管人)恢复它。简单来说Memento模式是关于以稍后可以以平滑方式恢复的方式捕获和存储对象的当前状态。维基百科说memento模式是一种软件设计模式,它提供将对象恢复到其先前状态的能力(通过回滚撤消)。当您需要提供某种撤消功能时通常很有用。程序化示例让我们举一个文本编辑器的例子,它不时地保存状态,你可以根据需要恢复。首先,我们有memento对象,可以保存编辑器状态class EditorMemento{ protected $content; public function __construct(string $content) { $this->content = $content; } public function getContent() { return $this->content; }}然后我们有我们的编辑器即即将使用memento对象的创作者class Editor{ protected $content = ‘’; public function type(string $words) { $this->content = $this->content . ’ ’ . $words; } public function getContent() { return $this->content; } public function save() { return new EditorMemento($this->content); } public function restore(EditorMemento $memento) { $this->content = $memento->getContent(); }}然后它可以用作$editor = new Editor();// Type some stuff$editor->type(‘This is the first sentence.’);$editor->type(‘This is second.’);// Save the state to restore to : This is the first sentence. This is second.$saved = $editor->save();// Type some more$editor->type(‘And this is third.’);// Output: Content before Savingecho $editor->getContent(); // This is the first sentence. This is second. And this is third.// Restoring to last saved state$editor->restore($saved);$editor->getContent(); // This is the first sentence. This is second.????观察者模式(Observer)现实世界的例子一个很好的例子是求职者,他们订阅了一些职位发布网站,只要有匹配的工作机会,他们就会得到通知。简单来说定义对象之间的依赖关系,以便每当对象更改其状态时,都会通知其所有依赖项。维基百科说观察者模式是一种软件设计模式,其中一个称为主体的对象维护其依赖者列表,称为观察者,并通常通过调用其中一种方法自动通知它们任何状态变化。程序化的例子从上面翻译我们的例子。首先,我们有求职者需要通知职位发布class JobPost{ protected $title; public function __construct(string $title) { $this->title = $title; } public function getTitle() { return $this->title; }}class JobSeeker implements Observer{ protected $name; public function __construct(string $name) { $this->name = $name; } public function onJobPosted(JobPost $job) { // Do something with the job posting echo ‘Hi ’ . $this->name . ‘! New job posted: ‘. $job->getTitle(); }}然后我们会找到求职者会订阅的招聘信息class EmploymentAgency implements Observable{ protected $observers = []; protected function notify(JobPost $jobPosting) { foreach ($this->observers as $observer) { $observer->onJobPosted($jobPosting); } } public function attach(Observer $observer) { $this->observers[] = $observer; } public function addJob(JobPost $jobPosting) { $this->notify($jobPosting); }}然后它可以用作// Create subscribers$johnDoe = new JobSeeker(‘John Doe’);$janeDoe = new JobSeeker(‘Jane Doe’);// Create publisher and attach subscribers$jobPostings = new EmploymentAgency();$jobPostings->attach($johnDoe);$jobPostings->attach($janeDoe);// Add a new job and see if subscribers get notified$jobPostings->addJob(new JobPost(‘Software Engineer’));// Output// Hi John Doe! New job posted: Software Engineer// Hi Jane Doe! New job posted: Software Engineer????访问者模式(Visitor)现实世界的例子考虑去迪拜的人。他们只需要一种方式(即签证)进入迪拜。抵达后,他们可以自己来迪拜的任何地方,而无需征求许可或做一些腿部工作,以便访问这里的任何地方; 让他们知道一个地方,他们可以访问它。访客模式可以让您做到这一点,它可以帮助您添加访问的地方,以便他们可以尽可能多地访问,而无需做任何腿部工作。简单来说访客模式允许您向对象添加更多操作,而无需修改它们。维基百科说在面向对象的编程和软件工程中,访问者设计模式是一种将算法与其运行的对象结构分离的方法。这种分离的实际结果是能够在不修改这些结构的情况下向现有对象结构添加新操作。这是遵循开放/封闭原则的一种方式。程序化的例子让我们举一个动物园模拟的例子,我们有几种不同的动物,我们必须让它们成为声音。让我们用访客模式翻译这个// Visiteeinterface Animal{ public function accept(AnimalOperation $operation);}// Visitorinterface AnimalOperation{ public function visitMonkey(Monkey $monkey); public function visitLion(Lion $lion); public function visitDolphin(Dolphin $dolphin);}然后我们有动物实施class Monkey implements Animal{ public function shout() { echo ‘Ooh oo aa aa!’; } public function accept(AnimalOperation $operation) { $operation->visitMonkey($this); }}class Lion implements Animal{ public function roar() { echo ‘Roaaar!’; } public function accept(AnimalOperation $operation) { $operation->visitLion($this); }}class Dolphin implements Animal{ public function speak() { echo ‘Tuut tuttu tuutt!’; } public function accept(AnimalOperation $operation) { $operation->visitDolphin($this); }}让我们实现我们的访客class Speak implements AnimalOperation{ public function visitMonkey(Monkey $monkey) { $monkey->shout(); } public function visitLion(Lion $lion) { $lion->roar(); } public function visitDolphin(Dolphin $dolphin) { $dolphin->speak(); }}然后它可以用作$monkey = new Monkey();$lion = new Lion();$dolphin = new Dolphin();$speak = new Speak();$monkey->accept($speak); // Ooh oo aa aa! $lion->accept($speak); // Roaaar!$dolphin->accept($speak); // Tuut tutt tuutt!我们可以通过为动物建立一个继承层次结构来做到这一点,但是每当我们不得不为动物添加新动作时我们就必须修改动物。但现在我们不必改变它们。例如,假设我们被要求向动物添加跳跃行为,我们可以通过创建新的访问者来添加它,即class Jump implements AnimalOperation{ public function visitMonkey(Monkey $monkey) { echo ‘Jumped 20 feet high! on to the tree!’; } public function visitLion(Lion $lion) { echo ‘Jumped 7 feet! Back on the ground!’; } public function visitDolphin(Dolphin $dolphin) { echo ‘Walked on water a little and disappeared’; }}并用于使用$jump = new Jump();$monkey->accept($speak); // Ooh oo aa aa!$monkey->accept($jump); // Jumped 20 feet high! on to the tree!$lion->accept($speak); // Roaaar!$lion->accept($jump); // Jumped 7 feet! Back on the ground!$dolphin->accept($speak); // Tuut tutt tuutt!$dolphin->accept($jump); // Walked on water a little and disappeared????策略模式(Strategy)现实世界的例子考虑排序的例子,我们实现了冒泡排序,但数据开始增长,冒泡排序开始变得非常缓慢。为了解决这个问题,我们实现了快速排序。但是现在虽然快速排序算法对大型数据集的效果更好,但对于较小的数据集来说速度非常慢。为了解决这个问题,我们实施了一个策略,对于小型数据集,将使用冒泡排序并进行更大规模的快速排序。简单来说策略模式允许您根据情况切换算法或策略。维基百科说在计算机编程中,策略模式(也称为策略模式)是一种行为软件设计模式,可以在运行时选择算法的行为。程序化的例子从上面翻译我们的例子。首先,我们有战略界面和不同的战略实施interface SortStrategy{ public function sort(array $dataset): array;}class BubbleSortStrategy implements SortStrategy{ public function sort(array $dataset): array { echo “Sorting using bubble sort”; // Do sorting return $dataset; }}class QuickSortStrategy implements SortStrategy{ public function sort(array $dataset): array { echo “Sorting using quick sort”; // Do sorting return $dataset; }}然后我们的客户将使用任何策略class Sorter{ protected $sorter; public function __construct(SortStrategy $sorter) { $this->sorter = $sorter; } public function sort(array $dataset): array { return $this->sorter->sort($dataset); }}它可以用作And it can be used as$dataset = [1, 5, 4, 3, 2, 8];$sorter = new Sorter(new BubbleSortStrategy());$sorter->sort($dataset); // Output : Sorting using bubble sort$sorter = new Sorter(new QuickSortStrategy());$sorter->sort($dataset); // Output : Sorting using quick sort????状态模式(State)现实世界的例子想象一下,你正在使用一些绘图应用程序,你选择绘制画笔。现在画笔根据所选颜色改变其行为,即如果你选择了红色,它会画成红色,如果是蓝色则会是蓝色等。简单来说它允许您在状态更改时更改类的行为。维基百科说状态模式是一种行为软件设计模式,它以面向对象的方式实现状态机。使用状态模式,通过将每个单独的状态实现为状态模式接口的派生类来实现状态机,并通过调用由模式的超类定义的方法来实现状态转换。状态模式可以解释为一种策略模式,它能够通过调用模式接口中定义的方法来切换当前策略。程序化的例子让我们以文本编辑器为例,它允许您更改键入的文本的状态,即如果您选择了粗体,则开始以粗体显示,如果是斜体,则以斜体显示等。首先,我们有状态接口和一些状态实现interface WritingState{ public function write(string $words);}class UpperCase implements WritingState{ public function write(string $words) { echo strtoupper($words); }}class LowerCase implements WritingState{ public function write(string $words) { echo strtolower($words); }}class DefaultText implements WritingState{ public function write(string $words) { echo $words; }}然后我们有编辑class TextEditor{ protected $state; public function __construct(WritingState $state) { $this->state = $state; } public function setState(WritingState $state) { $this->state = $state; } public function type(string $words) { $this->state->write($words); }}然后它可以用作$editor = new TextEditor(new DefaultText());$editor->type(‘First line’);$editor->setState(new UpperCase());$editor->type(‘Second line’);$editor->type(‘Third line’);$editor->setState(new LowerCase());$editor->type(‘Fourth line’);$editor->type(‘Fifth line’);// Output:// First line// SECOND LINE// THIRD LINE// fourth line// fifth line????<模板方法模式(Template Method)现实世界的例子假设我们正在建造一些房屋。构建的步骤可能看起来像准备房子的基地建造墙壁添加屋顶添加其他楼层这些步骤的顺序永远不会改变,即在建造墙壁等之前不能建造屋顶,但是每个步骤都可以修改,例如墙壁可以由木头或聚酯或石头制成。简单来说模板方法定义了如何执行某个算法的框架,但是将这些步骤的实现推迟到子类。维基百科说在软件工程中,模板方法模式是一种行为设计模式,它定义了操作中算法的程序框架,将一些步骤推迟到子类。它允许重新定义算法的某些步骤而不改变算法的结构。程序化示例想象一下,我们有一个构建工具,可以帮助我们测试,lint,构建,生成构建报告(即代码覆盖率报告,linting报告等),并在测试服务器上部署我们的应用程序。首先,我们有基类,它指定构建算法的骨架abstract class Builder{ // Template method final public function build() { $this->test(); $this->lint(); $this->assemble(); $this->deploy(); } abstract public function test(); abstract public function lint(); abstract public function assemble(); abstract public function deploy();}然后我们可以实现我们的实现class AndroidBuilder extends Builder{ public function test() { echo ‘Running android tests’; } public function lint() { echo ‘Linting the android code’; } public function assemble() { echo ‘Assembling the android build’; } public function deploy() { echo ‘Deploying android build to server’; }}class IosBuilder extends Builder{ public function test() { echo ‘Running ios tests’; } public function lint() { echo ‘Linting the ios code’; } public function assemble() { echo ‘Assembling the ios build’; } public function deploy() { echo ‘Deploying ios build to server’; }}然后它可以用作$androidBuilder = new AndroidBuilder();$androidBuilder->build();// Output:// Running android tests// Linting the android code// Assembling the android build// Deploying android build to server$iosBuilder = new IosBuilder();$iosBuilder->build();// Output:// Running ios tests// Linting the ios code// Assembling the ios build// Deploying ios build to server????总结一下那就是把它包起来。我将继续改进这一点,因此您可能希望观看/加注此存储库以重新访问。此外,我计划对架构模式进行相同的编写,请继续关注它。 ...

March 6, 2019 · 14 min · jiezi

个推微服务网关架构实践

作者:个推应用平台基础架构高级研发工程师 阿飞在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请求,客户端可能通过调用N个微服务的接口完成一个用户请求。因此,在客户端和服务端之间增加一个API网关成为多数微服务架构的必然选择。在个推的微服务实践中,API网关也起着至关重要的作用。一方面,API网关是个推微服务体系对外的唯一入口;另一方面,API网关中实现了很多后端服务的共性需求,避免了重复建设。个推微服务网关的设计与实现个推微服务主要是基于Docker和Kubernetes进行实践的。在整个微服务架构中,最底层的是个推私有部署的Kubernetes集群,在集群之上,部署了应用服务。个推的应用服务体系共分为三层,最上一层是网关层,接着是业务层,最下面是基础层服务。在部署应用服务时,我们使用了Kubernetes的命名空间对不同产品线的产品进行隔离。除了应用服务外, Kubernetes集群上还部署了Consul来实现配置的管理、Kube-DNS实现服务注册与发现,以及一些辅助系统来进行应用和集群的管理。下图是个推微服务体系的架构图。个推对API网关的功能需求主要有以下几方面:要支持配置多个产品,为不同的产品提供不同的端口;动态路由;URI的重写;服务的注册与发现;负载均衡;安全相关的需求,如session校验等;流量控制;链路追踪;A/B Testing。在对市面上已有的网关产品进行调研后,我们的技术团队发现,它们并不太适合应用于个推的微服务体系。第一,个推配置的管理都是基于Consul实现的,而大部分网关产品都需要基于一些DB存储,来进行配置的管理;第二,大部分的网关产品提供的功能比较通用,也比较完善,这同时也降低了配置的复杂度以及灵活性;第三,大部分的网关产品很难直接融入到个推的微服务架构体系中。最终,个推选择使用了OperResty和Lua进行自研网关,在自研的过程中,我们也借鉴了其他网关产品的一些设计,如Kong和Orange的插件机制等。个推的API网关的插件设计如下图所示。OpenResty对请求的处理分为多个阶段。个推API网关的插件主要是在Set、Rewrite、Access、Header_filter、Body_filter、Log这六个阶段做相应的处理,其中,每一个插件都可以在一个或多个阶段起到相应的作用。在一个请求到达API网关之后,网关会根据配置为该请求选择插件,然后根据每个插件的规则,进一步过滤出匹配规则的插件,最后对插件进行实例化,对流量进行相应的处理。我们可以通过举例来理解这个过程,如上图所示,localhost:8080/api/demo/test/hello这个请求到达网关后,网关会根据host和端口确定产品信息,并提取出URI(/api/demo/test/hello),然后根据产品的具体配置,筛选出需要使用的插件——Rewrite_URI、Dyups和Auth,接下来根据每个插件的规则配置进行过滤,过滤后,只有Rewrite_URI和Dyups两个插件被选中。之后实例化这两个插件,在各个阶段对请求进行处理。请求被转发到后端服务时,URI就被rewrite为“/demo/test/hello”,upstream也被设置为“prod1-svc1”。请求由后端服务处理之后,响应会经网关返回给客户端,这就是整个插件的设计和工作的流程。为了优化性能,我们将插件的实例化延缓到了请求真正开始处理时,在此之前,网关会通过产品配置和规则,过滤掉不需要执行的插件。从图中也可以看出,每个插件的规则配置都很简单,并且没有统一的格式,这也确保了插件配置的简单灵活。网关的配置均为热更新,通过Consul和Consul-Template来实现,配置在Consul上进行更新后,Consul-Template会将其实时地拉取下来,然后通过以下两种方式进行更新。(1)通过调用Update API,将配置更新到shared-dict中。(2)更新配置文件,利用Reload OpenResty实现配置文件的更新。个推微服务网关提供的主要功能1.动态路由动态路由主要涉及到三个方面:服务注册、服务发现和请求转发。如下图所示,服务的注册和发现是基于Kubernetes的Service和Kube-DNS实现的,在Consul中,会维持一个服务的映射表,应用的每一个微服务都对应Kubernetes上的一个Service,每创建一个Service都会在Consul上的服务映射表中添加一项(会被实时更新到网关的共享内存中)。网关每收到一个请求都会从服务映射表中查询到具体的后端服务(即Kubernetes中的Service名),并进行动态路由。Kube-DNS可以将Service的域名解析成Kubernetes内部的ClusterIP,而Service代理了多个Pod,会将流量均衡地转发到不同的Pod上。2.流量控制流量控制主要是通过一个名为“Counter”的后端服务和网关中的流控插件实现的。Counter负责存储请求的访问次数和限值,并且支持按时间维度进行计数。流控插件负责拦截流量,调用Counter的接口进行超限查询,如果Counter返回请求超限,网关就会直接拒绝访问,实现限次的功能,再结合时间维度就可以实现限频的需求。同时流控插件通过输出日志信息到fluent-bit,由fluent-bit聚合计次来更新Counter中的计数。3.链路追踪整个微服务体系的链路追踪是基于分布式的链路追踪系统Zipkin来实现的。通过在网关安装Zipkin插件和在后端服务中引入Zipkin中间件,实现最终的链路追踪功能。具体架构如下图所示。4. A/B测试在A/B测试的实现中,有以下几个关键点:(1)所有的策略信息都配置在Consul上,并通过Consul-Template实时生效到各个微服务的内存中;(2)每条策略均有指明,调用一个微服务时应调用A还是B(默认为A);(3)网关中实现A/B插件,在请求到达网关时,通过A/B插件配置的规则,即可确定请求适用的A/B策略;(4)网关会将请求适用的A/B策略通过URL参数传递下去;(5)每个微服务通过传递下来的策略,选择正确的服务进行访问。下图给出了两种场景下的调用链路。总结以上就是个推微服务网关的设计和主要功能的实现。之后,个推的技术团队会不断提升API网关的弹性设计,使其能够在故障出现时,缩小故障的影响范围;同时,我们也会继续将网关与DevOps平台做进一步地结合,以确保网关在迭代更新时,能够有更多的自动化测试来保证质量,实现更快速地部署。

March 5, 2019 · 1 min · jiezi

Spring Cloud + Netty 打造分布式可集群部署的 DHT 磁力爬虫(开源)

演示地址: https://dodder.cc三年前,照着 Python 版的 DHT 网络爬虫用 Java 重写了一遍,当时大学还未毕业,写出来的代码比较杂乱,数据跑到 1600 万的时候就遇到了瓶颈,最近辞职了想学习一波 Spring Cloud 微服务开发,于是就有了现在这个项目。学习一门新的技术总是枯燥无味的,不知道大家有没有这样的感觉,照着官方文档或者 Demo 去敲代码,不仅印象不深刻容易忘记,而且特别无聊,所以对于我个人而言,在学习一门新技术时喜欢去找一个自己感兴趣的实战场景,然后再进行学习实践,遇到不懂的再去 Google 或者看官方文档,而不是按部就班地买本书一页一页的全部看完,只有自己要用到什么的时候再去查相关的文档,这样子对应用场景以及技术要点才会更加深刻。闲话不多说了,接下来谈谈 DHT 网络爬虫吧。对于老湿鸡来说,到磁力搜索网站去搜索番号是他们获取资源的一种快速途径,对于这种网站,广告也是一大堆的,那么身为 IT 技术人,能不能自己来实现一个呢?答案肯定是可以的,而且技术含量也不是特别高,但实现之前,Bittorrent 协议以及其 Wire Peer 扩展协议,还是必须要了解的,前者在官方文档还能详细找到,后者在维基百科上貌似已经被删掉了(官方文档里只有简单的介绍了,整个协议数据收发的流程已经没有了)。参考协议: http://www.bittorrent.org/bep… http://www.bittorrent.org/bep…除此之外,还需要熟悉 Torrent 种子文件里的结构信息,Bittorrent 协议以及 Torrent 种子文件信息大多都采用 bencode 编码,学习之前,可以先去了解一下这个编码,和 json 有点类似,不过也不是必要的,不用自己去实现,有现成的编解码库。对前面的东西了解过后,再整理下整个爬取的流程:使用 Netty 本地模拟一个 DHT 网络节点加入到 DHT 网络中去(即向启动节点发送 find_node 请求)收到 find_node 回复解析出更多的 DHT 节点信息,向这些节点发送 find_node 节点(目的就是让更多的人知道自己,专业术语就是把自己的节点 ID 加入到对方的桶里,和推销自己差不多吧~)那些收到 find_node 请求的节点,把我们的节点 ID 加入到桶里之后,它就会向我们发送 find_node、get_peers、announce_peer 请求。对于其他节点发送给我们的请求,我们需要根据协议进行回复,否则对方会认为我们是个不活跃节点把我们从对方的桶里删除,并且不会发送 announce_peer 消息给我们对于 get_peers 消息里的 info_hash 我们可以进行忽略,因为此时对方也是在查找对应 info_hash 的种子文件,只有收到 announce_peer 消息时的 info_hash 对我们才有用,因为它表示当前对方正在指定端口下载该种子文件的 metadata 信息。根据 announce_peer 中的 info_hash 到指定 ip 端口使用 wire peer 扩展协议进行 metadata 数据的交换(即下载)下载成功,解析出种子文件列表信息入库。具体实现请参考我的开源项目: https://github.com/xwlcn/Dodder代码如有问题,欢迎指正,仅供技术交流,切勿用作非法、商业用途。 ...

March 5, 2019 · 1 min · jiezi

重拾-Spring AOP-自动代理

概述在上一篇 重拾-Spring AOP 中我们会发现 Spring AOP 是通过类 ProxyFactoryBean 创建代理对象,其有个缺陷就是只能代理一个目标对象 bean, 当代理目标类过多时,配置文件臃肿不方便管理维护,因此 Spring 提供了能够实现自动创建代理的类 BeanNameAutoProxyCreator , DefaultAdvisorAutoProxyCreator ;下面我们看下二者是如何实现自动代理的。BeanNameAutoProxyCreatorBeanNameAutoProxyCreator 是通过判断当前 bean name 是否匹配,只有匹配的 bean 才会创建代理。使用示例Spring xml 配置<bean id=“userService” class=“org.springframework.aop.UserServiceImpl” /><bean id=“demoService” class=“org.springframework.aop.DemoServiceImpl” /><bean id=“userBeforeAdvice” class=“org.springframework.aop.UserBeforeAdvice” /><bean id=“userAfterAdvice” class=“org.springframework.aop.UserAfterAdvice” /><bean id=“beanNameAutoProxyCreator” class=“org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator”> <!– 配置要代理的 bean –> <property name=“beanNames”> <list> <value>userService</value> <value>demoService</value> </list> </property> <!– 配置 interceptor, advice, advisor –> <property name=“interceptorNames”> <list> <value>userAfterAdvice</value> <value>userBeforeAdvice</value> </list> </property></bean>测试ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/org/springframework/aop/aop.xml");UserService userService = (UserService) ctx.getBean(“userService”);userService.say();DemoService demoService = (DemoService) ctx.getBean(“demoService”);demoService.demo();运行结果do before advice ….do say methoddo after return advice ….do before advice ….do demo.do after return advice ….实现分析类结构如上图 BeanNameAutoProxyCreator 类结构可以看出,其实现了接口 BeanPostProcessor ; 那么我们可以大概猜测出其自动代理的实现原理与自动注入类似,都是在 bean 实例化后进行特殊的处理,下面就让我们看下源码验证下吧。分析public Object postProcessAfterInitialization(Object bean, String name) throws BeansException { // Check for special cases. We don’t want to try to autoproxy a part of the autoproxying // infrastructure, lest we get a stack overflow. if (isInfrastructureClass(bean, name) || shouldSkip(bean, name)) { logger.debug(“Did not attempt to autoproxy infrastructure class ‘” + bean.getClass() + “’”); return bean; } TargetSource targetSource = getTargetSource(bean, name); Object[] specificInterceptors = getInterceptorsAndAdvisorsForBean(bean, name); // proxy if we have advice or if a TargetSourceCreator wants to do some // fancy stuff such as pooling if (specificInterceptors != DO_NOT_PROXY || !(targetSource instanceof SingletonTargetSource)) { // handle prototypes correctly // 获取容器中配置的 advisors Advisor[] commonInterceptors = resolveInterceptorNames(); List allInterceptors = new ArrayList(); if (specificInterceptors != null) { allInterceptors.addAll(Arrays.asList(specificInterceptors)); if (commonInterceptors != null) { if (this.applyCommonInterceptorsFirst) { allInterceptors.addAll(0, Arrays.asList(commonInterceptors)); } else { allInterceptors.addAll(Arrays.asList(commonInterceptors)); } } } if (logger.isInfoEnabled()) { int nrOfCommonInterceptors = commonInterceptors != null ? commonInterceptors.length : 0; int nrOfSpecificInterceptors = specificInterceptors != null ? specificInterceptors.length : 0; logger.info(“Creating implicit proxy for bean ‘” + name + “’ with " + nrOfCommonInterceptors + " common interceptors and " + nrOfSpecificInterceptors + " specific interceptors”); } ProxyFactory proxyFactory = new ProxyFactory(); // copy our properties (proxyTargetClass) inherited from ProxyConfig proxyFactory.copyFrom(this); if (!getProxyTargetClass()) { // Must allow for introductions; can’t just set interfaces to // the target’s interfaces only. // 添加设置代理的接口 Class[] targetsInterfaces = AopUtils.getAllInterfaces(bean); for (int i = 0; i < targetsInterfaces.length; i++) { proxyFactory.addInterface(targetsInterfaces[i]); } } for (Iterator it = allInterceptors.iterator(); it.hasNext();) { Advisor advisor = GlobalAdvisorAdapterRegistry.getInstance().wrap(it.next()); // 添加 advisor proxyFactory.addAdvisor(advisor); } proxyFactory.setTargetSource(getTargetSource(bean, name)); // 创建代理对象,依旧采用的 jdk 动态代理; 因为上面设置了代理的 interface return proxyFactory.getProxy(); } else { return bean; }}protected Object[] getInterceptorsAndAdvisorsForBean(Object bean, String beanName) { if (this.beanNames != null) { // bean name 包含在配置的名称列表中,说明需要代理 if (this.beanNames.contains(beanName)) { return PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS; } for (Iterator it = this.beanNames.iterator(); it.hasNext();) { String mappedName = (String) it.next(); // bean name 匹配通配符,说明需要代理 if (isMatch(beanName, mappedName)) { return PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS; } } } // 说明 bean 不需要代理 return DO_NOT_PROXY;}protected boolean isMatch(String beanName, String mappedName) { // bean name 匹配通配符 return (mappedName.endsWith("") && beanName.startsWith(mappedName.substring(0, mappedName.length() - 1))) || (mappedName.startsWith("") && beanName.endsWith(mappedName.substring(1, mappedName.length())));}从 BeanNameAutoProxyCreator 的源码大概总结其自动代理流程:判断当前 bean name 是否匹配配置加载配置的 advisor, 也就是配置的 interceptorNames采用 jdk 动态代理创建 bean 的代理对象DefaultAdvisorAutoProxyCreatorDefaultAdvisorAutoProxyCreator 会搜索 BeanFactory 容器内部所有可用的 Advisor; 并为容器中匹配的 bean 创建代理。 在上一篇 重拾-Spring AOP 中我们知道 Spring AOP 会默认创建实例为 DefaultPointcutAdvisor 的 Advisor; 那么在分析 DefaultAdvisorAutoProxyCreator 之前,我们看下 Spring AOP 还为我们提供了哪些内置的 Advisor 。NameMatchMethodPointcutAdvisorNameMatchMethodPointcutAdvisor 是按 method name 匹配,只有当目标类执行方法匹配的时候,才会执行 Advicepublic class NameMatchMethodPointcut extends StaticMethodMatcherPointcut { // 配置拦截的 method name private String[] mappedNames = new String[0]; public boolean matches(Method m, Class targetClass) { for (int i = 0; i<this.mappedNames.length; i++) { String mappedName = this.mappedNames[i]; // 目标方法是否与配置的 method name 相等;或者匹配通配符 if (mappedName.equals(m.getName()) || isMatch(m.getName(), mappedName)) { return true; } } return false; } // 是否以 * 开头或结束并匹配 protected boolean isMatch(String methodName, String mappedName) { return (mappedName.endsWith("") && methodName.startsWith(mappedName.substring(0, mappedName.length() - 1))) || (mappedName.startsWith("") && methodName.endsWith(mappedName.substring(1, mappedName.length()))); }}RegexpMethodPointcutAdvisorRegexpMethodPointcutAdvisor 是按照正则表达式匹配方法,能够精确定位到需要拦截的方法。public class RegexpMethodPointcut extends StaticMethodMatcherPointcut implements ClassFilter { public boolean matches(Method m, Class targetClass) { // TODO use target class here? // 拼接表达式 String patt = m.getDeclaringClass().getName() + “.” + m.getName(); for (int i = 0; i < this.compiledPatterns.length; i++) { // 正则匹配 boolean matched = this.matcher.matches(patt, this.compiledPatterns[i]); if (logger.isDebugEnabled()) { logger.debug(“Candidate is: ‘” + patt + “’; pattern is " + this.compiledPatterns[i].getPattern() + “; matched=” + matched); } if (matched) { return true; } } return false; } public boolean matches(Class clazz) { // TODO do with regexp return true; } public ClassFilter getClassFilter() { return this; }}使用示例xml 配置<beans> <bean id=“userService” class=“org.springframework.aop.UserServiceImpl” /> <bean id=“demoService” class=“org.springframework.aop.DemoServiceImpl” /> <bean id=“userBeforeAdvice” class=“org.springframework.aop.UserBeforeAdvice” /> <bean id=“userAfterAdvice” class=“org.springframework.aop.UserAfterAdvice” /> <!– 按方法名称匹配 –> <bean id=“nameMatchMethodPointcutAdvisor” class=“org.springframework.aop.support.NameMatchMethodPointcutAdvisor”> <property name=“mappedNames”> <!– 匹配 save 开头的方法 –> <value>save*</value> </property> <property name=“advice”> <ref bean=“userBeforeAdvice” /> </property> </bean> <bean id=“regexpMethodPointcutAdvisor” class=“org.springframework.aop.support.RegexpMethodPointcutAdvisor”> <property name=“pattern”> <!– 匹配以 del 开头的方法 –> <value>org.springframework.aop..del.*</value> </property> <property name=“advice”> <ref bean=“userAfterAdvice” /> </property> </bean> <bean class=“org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator” /></beans>测试ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/org/springframework/aop/aop.xml”);UserService userService = (UserService) ctx.getBean(“userService”);userService.saveUser();userService.delUser();DemoService demoService = (DemoService) ctx.getBean(“demoService”);demoService.saveDemo();demoService.delDemo();测试结果do before advice ….do save user ……do del user ……do after return advice ….do before advice ….do save demo ……do del demo ……do after return advice ….从测试结果可以看出,通过配置不同 Advisor 匹配不同的 Method 采用相应的 Advice 进行处理。实现分析类结构从上图 DefaultAdvisorAutoProxyCreator 类结构,我们知道其实现与 BeanNameAutoProxyCreator 类似;都是通过实现接口 BeanPostProcessor 在 bean 完成实例化后进行自动代理处理。分析因 DefaultAdvisorAutoProxyCreator 和 BeanNameAutoProxyCreator 都继承了类 AbstractAutoProxyCreator ,所以从源码中我们可以发现二者都重写了方法 getInterceptorsAndAdvisorsForBean ,也就是在获取当前 bean 所匹配的 Advisor 逻辑不一样之外其他处理一致; 那么下面针对 DefaultAdvisorAutoProxyCreator 的实现我们主要看下方法 getInterceptorsAndAdvisorsForBean 的处理。protected Object[] getInterceptorsAndAdvisorsForBean(Object bean, String name) { // 查找与当前 bean 匹配的 advisor List advices = findEligibleAdvisors(bean.getClass()); if (advices.isEmpty()) { return DO_NOT_PROXY; } // 对 advisor 集合排序 advices = sortAdvisors(advices); return advices.toArray();}查找匹配的 Advisorprotected List findEligibleAdvisors(Class clazz) { // 查找当前容器中所有定义的 advisor List candidateAdvice = findCandidateAdvisors(); List eligibleAdvice = new LinkedList(); for (int i = 0; i < candidateAdvice.size(); i++) { // Sun, give me generics, please! Advisor candidate = (Advisor) candidateAdvice.get(i); // 判断 bean 是否可以应用 advisor if (AopUtils.canApply(candidate, clazz, null)) { // 将 advisor 添加到匹配的集合中 eligibleAdvice.add(candidate); logger.info(“Candidate Advice [” + candidate + “] accepted for class [” + clazz.getName() + “]”); } else { logger.info(“Candidate Advice [” + candidate + “] rejected for class [” + clazz.getName() + “]”); } } return eligibleAdvice;}获取容器中所有的 Advisorprotected List findCandidateAdvisors() { if (!(getBeanFactory() instanceof ListableBeanFactory)) { throw new IllegalStateException(“Cannot use DefaultAdvisorAutoProxyCreator without a ListableBeanFactory”); } ListableBeanFactory owningFactory = (ListableBeanFactory) getBeanFactory(); // 从容器中查找所有 bean 定义 type 为 Advisor 的 bean name String[] adviceNames = BeanFactoryUtils.beanNamesIncludingAncestors(owningFactory, Advisor.class); List candidateAdvisors = new LinkedList(); for (int i = 0; i < adviceNames.length; i++) { String name = adviceNames[i]; if (!this.usePrefix || name.startsWith(this.advisorBeanNamePrefix)) { // 获取 advisor 实例 Advisor advisor = (Advisor) owningFactory.getBean(name); candidateAdvisors.add(advisor); } } return candidateAdvisors;}判断 bean 是否匹配 Advisorpublic static boolean canApply(Advisor advisor, Class targetClass, Class[] proxyInterfaces) { if (advisor instanceof IntroductionAdvisor) { return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); } else if (advisor instanceof PointcutAdvisor) { PointcutAdvisor pca = (PointcutAdvisor) advisor; // 通过 advisor 的 pointcut 判断 bean 是否匹配 return canApply(pca.getPointcut(), targetClass, proxyInterfaces); } else { // It doesn’t have a pointcut so we assume it applies return true; }}public static boolean canApply(Pointcut pc, Class targetClass, Class[] proxyInterfaces) { // 类是否匹配 if (!pc.getClassFilter().matches(targetClass)) { return false; } // 判断类中的 method 是否匹配 // 获取类下所有的method Method[] methods = targetClass.getMethods(); for (int i = 0; i < methods.length; i++) { Method m = methods[i]; // If we’re looking only at interfaces and this method // isn’t on any of them, skip it if (proxyInterfaces != null && !methodIsOnOneOfTheseInterfaces(m, proxyInterfaces)) { continue; } // 执行 pointcut 的 method match if (pc.getMethodMatcher().matches(m, targetClass)) return true; } return false;}从 DefaultAdvisorAutoProxyCreator 的源码分析,可知其自动代理流程大概如下:从容器中获取所有 Advisor 实例匹配 bean 所支持的 Advisor采用 jdk 动态代理创建 bean 的代理对象小结从 BeanNameAutoProxyCreator , DefaultAdvisorAutoProxyCreator 二者的实现可以看出其相同点都是基于实现接口 BeanPostProcessor 的实现都是先获取当前 bean 所匹配的 Advisor,后在创建代理对象二者的不同点在于:前者是基于 bean name 判断是否判断,后者是通过 Advisor 内部的 Ponitcut 匹配判断前者的 Advisor 是用户配置的,后者是容器中所有匹配的 Advisor ...

March 5, 2019 · 6 min · jiezi

spring中过滤器与拦截器的区别

spring中过滤器介绍功能:使用场景:spring中拦截器介绍功能:使用场景:两者区别

March 4, 2019 · 1 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

aop初探

在本周的项目中第一次尝试了aop这个鼎鼎大名的东西,以前一直觉得这个东西会很难理解,就没有接触,不过再真正接触以后发现基本的使用还是很简单的,当然有这种感觉少不了学长的帮助,感谢张喜硕学长。aopaop是什么呢?用于干什么?AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中。即aop的作用就是去掉代码的冗余,使程序的结构更加清晰。虽然去除冗余代码也可一通过抽象继承来实现,但这会让你继承或实现一些和业务并不相关的类或接口。spring aop的用法spring的aop是通过动态代理实现的。代理模式代理模式的定义如下:为其他对象提供一种代理以控制对这个对象的访问。比如A对象要做一件事情,在没有代理前,自己来做,在对A代理后,由A的代理类B来做。代理其实是在原实例前后加了一层处理,这也是AOP的初级轮廓。代理又分为静态代理和动态代理,这里不再细说,想要更深入的了解可以看看这篇文章而要如何在spring中使用aop呢?不要着急,接着往下看:首先便是要知道切面应该用在那了,对此你可以使用很多方法:execution:用于匹配方法执行的连接点;within:用于匹配指定类型内的方法执行;this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;@within:用于匹配所以持有指定注解类型内的方法;@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;@annotation:用于匹配当前执行方法持有指定注解的方法;就像这样:当然关于各个参数的具体用法,可以另写一篇文章了,如果想要了解可以参看这篇文章。在上面的方法中就是切了有HostOwnerCheck这个注解的方法然后然后就是编写你对这个方法有些什么操作,可以分别编写方法执行前,执行后……要做什么比如下面就是方法执行前判断是否有权限,如果没有就抛出一个方法:这样,一个简单的切面就完成了。参考文章Spring AOP就是这么简单啦spring AOP是什么?你都拿它做什么?

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 Cloud Alibaba迁移指南(三):极简的 Config

自 Spring Cloud 官方宣布 Spring Cloud Netflix 进入维护状态后,我们开始制作《Spring Cloud Alibaba迁移指南》系列文章,向开发者提供更多的技术选型方案,并降低迁移过程中的技术难度。第一篇:一行代码从 Hystrix 迁移到 Sentinel第二篇:零代码替换 Eureka第三篇,我们一起来看看 Spring Cloud Alibaba 是如何使用极简的方式来做到分布式应用的外部化配置,使得应用在运行时动态更新某些配置成为可能。 目前关于 Spring Cloud Config 的标准实现开源方面有三个,分别是:Spring Cloud Alibaba Nacos ConfigSpring Cloud Consul ConfigSpring Cloud Config (Spring Cloud 官方集成的方式)那面对于这么多的实现,Spring Cloud Alibaba Nacos Config 的实现它具有哪些优势呢?大致从以下几个方面来全方位的分析。 Spring Cloud Alibaba Nacos ConfigSpring Cloud Consul ConfigSpring Cloud Config (Spring Cloud 官方集成的方式)配置存储直接依赖于 Nacos。直接依赖于 Consul。通常的组合是Config-server 和 git。配置刷新无需人工干预,自动秒级刷新。无需人工干预,自动秒级刷新。需要人工干预,手动触发/bus/refresh 接口,才能达到配置动态刷新的效果。是否集成第三方服务不需要。不需要。存储需要依赖于git,刷新依赖于 RabbitMQ 。运维组件只需要运维 Nacos 本身即可。只需要运维 Consul本身。通常是要运维 Config-erver,MQ 的服务,提供存储能力的 Git。比较重的第三方依赖无,直接引入starter 即可 。无,直接引入 starter 即可。不仅需要引入 starter,而且还需要引入配置刷新依赖的 spring-cloud-starter-bus-amqp 。推送状态支持无无更新历史查询支持无无配置回滚支持无无配置加解密支持待确认待确认多重容灾支持无无同时 Spring Cloud Alibaba 还可以基于 Spring Cloud Alibaba Nacos Config 无缝对接云上的 ACM,这给一些需要上云的用户带来了极其的方便。综上全方位的对比,Spring Cloud Alibaba Nacos Config 无疑提供了性价比最高的 Spring Cloud Config 的开源实现。下面以一个快速上手的案例体验一下 Spring Cloud Alibaba Nacos Config 的实现是如何使用的。同时也提供了简单的方式给那些想转用 Spring Cloud Alibaba Nacos Config 的同学做一些参考。第 1 步:Nacos 服务端初始化。1.1 启动 Nacos Server。启动方式可见 Nacos 官网 。1.2 添加配置。启动好 Nacos 之后,在 Nacos 控制台添加如下的配置。Data ID: ${spring.application.name}.propertiesGroup : DEFAULT_GROUP配置格式: Properties配置内容: ${key}=${value}注意:Data Id 是以 properties(默认的文件扩展名方式)为扩展名。文件名以 &dollar;{spring.application.name} 配置参数为主。配置内容:当你想从其他的存储源(例如: git) 要往 Nacos 进行迁移的话,目前只能通过手动的方式进行逐个的添加。&dollar;{key} 是根据您的业务场景需要配置的或者迁移的 key, &dollar;{value} 就是对应的具体值。第 2 步:Spring Cloud Alibaba Nacos Config 客户端使用方式。2.1 添加 maven 依赖。为了能够在应用程序中使用 Nacos 来实现应用的外部化配置,在构建应用的同时或者已经存在的应用需要引入一个 Starter,如下所示:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>0.2.2.BUILD-SNAPSHOT</version></dependency>2.2 添加相关配置。客户端需要和 Nacos 服务端进行通信,因此需要配置 Nacos 服务端的地址。在您的应用配置文件中新增如下配置,这里以 application.properties 为例。spring.cloud.nacos.config.server-addr=127.0.0.1:8848完成以上两个步骤,就已经完成了 Spring Cloud Alibaba Nacos Config 的基本使用。完整的使用可参考 Spring Cloud Alibaba 的管方 Wiki 文档。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 28, 2019 · 1 min · jiezi

一个奇怪的问题:tomcat 栈溢出 StackOverflowError错误

一个栈溢出错误:ava.lang.StackOverflowError at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at >>>>>>>>>>>>>>>>>>> 中间省略几百行同样的内容 <<<<<<<<<<<<<<<<<<<<<<< org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.findNext(ApplicationHttpRequest.java:996) at org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator.hasMoreElements(ApplicationHttpRequest.java:971) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:873) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:869) at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:64) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728)tomcat版本是8.5.38版本,spring是4.2.6.RELEASE,出错的代码也找到了:DispatcherServlet:protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { if (logger.isDebugEnabled()) { String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : “”; logger.debug(“DispatcherServlet with name ‘” + getServletName() + “’” + resumed + " processing " + request.getMethod() + " request for [" + getRequestUri(request) + “]”); } // Keep a snapshot of the request attributes in case of an include, // to be able to restore the original attributes after the include. Map<String, Object> attributesSnapshot = null; if (WebUtils.isIncludeRequest(request)) { attributesSnapshot = new HashMap<String, Object>(); Enumeration<?> attrNames = request.getAttributeNames(); while (attrNames.hasMoreElements()) { //这个地方死循环了 String attrName = (String) attrNames.nextElement(); if (this.cleanupAfterInclude || attrName.startsWith(“org.springframework.web.servlet”)) { attributesSnapshot.put(attrName, request.getAttribute(attrName)); } } } …..} ApplicationHttpRequest中的内部类AttributeNamesEnumerator: @Override public boolean hasMoreElements() { return ((pos != last) || (next != null) || ((next = findNext()) != null)); } protected String findNext() { String result = null; while ((result == null) && (parentEnumeration.hasMoreElements())) { String current = parentEnumeration.nextElement(); if (!isSpecial(current)) { result = current; } } return result; }从错误日志上来看,先调用hasMoreElements,再调用findNext, 以此为循环,直到栈溢出。不知道怎么回事,只找到一篇tomcat的issue,有知道的指点一些。 ...

February 28, 2019 · 2 min · jiezi

程序员笔记——Spring基本概念速览

(一)Spring IoC重要概念1、控制反转(Inversion of control):控制反转是一种通过描述(在java中通过xml或者注解)并通过第三方去产生或获取特定对象的方式。控制反转IoC(Inversion of Control)是说创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方,比如转移交给了IoC容器,它就是一个专门用来创建对象的工厂,你要什么对象,它就给你什么对象,有了 IoC容器,依赖关系就变了,原先的依赖关系就没了,它们都依赖IoC容器了,通过IoC容器来建立它们之间的关系。控制反转就是获取依赖对象的方式反转了,正常情况下由应用程序主动创建依赖对象,实现对依赖对象的管理,创建依赖对象的控制权在应用程序手中,应用程序需要什么对象,就主动去创建这个对象,这是正转的情况。实现控制反转之后,由IoC容器实现依赖对象的创建和管理,应用程序需要什么样的对象,IoC容器就根据需求创建这个对象,应用程序只是被动地接收和使用这个对象,依赖对象的创建管理控制权由应用程序转移给了IoC容器,这就实现了控制反转。2、依赖注入(Dependency Injection):控制反转的另一种表述方式,即让调用类对某一接口的实现类的依赖关系由第三方(容器或协作类)注入,用以移除调用类对某一接口实现类的依赖。3、Beanfacory 和ApplicationContext:Spring通过配置文件描述Bean以及Bean之间的依赖关系,利用Java语言的反射功能实例化Bean并建立起Bean之间的依赖关系。Spring的IoC容器在完成这些底层工作的基础上,提供了Bean实例缓存、生命周期管理、Bean实例代理、事件发布、资源装载等服务。Beanfacory 是Spring框架最核心的接口,提供了高级IoC的配置机制。Beanfacory使管理不同的java对象成为可能,ApplicationContext(应用上下文)建立在Beanfacory基础之上,提供更多面向引用的功能。Beanfacory 即为IoC容器,由于ApplicationContext建立在Beanfacory,我们也称ApplicationContext为IoC容器。IoC容器主要功能1、动态创建、注入依赖对象。2、管理对象生命周期。3、映射依赖关系。实现IoC容器的方式:1、依赖查找。2、依赖注入。依赖注入的三种方式:1、构造器注入。2、setter注入。3、接口注入。注入和装配的区别:注入是实例化一个类时对类中各个参数的赋值方式。装配是定义bean以及bean之间关系。装配bean概述:1、基于xml中配置。2、基于注解中配置。3、基于java类配置。4、基于Groovy DSL配置。Bean作用域:1、单例(singleton):它是默认的选项,在整个应用中,Spring只为其生成一个Bean的实例。2、原型(prototype):每次注入时,或者通过Spring IoC容器获取Bean时,Spring都会为它创建一个新的实例。3、会话(session):在web应用中使用,就是在会话过程中Spring只会创建一个实例。4、请求(request):在web应用中使用,就是在一次请求中Spring会创建一个实例,但是不同的请求会创建不同的实例。基于xml中配置1、四种自动装配类型:(1)byName:根据名字自行自动匹配。(2)byType:根据类型自行自动匹配。(3)construtor:根据构造函数自行自动匹配。(4)autodetect:根据bean的自省机制选择byType或者construtor。2、Bean之间的关系:(1)继承;(2)依赖;(3)引用。基于注解的配置1、使用注解定义bean@Component:用于对所有的类进行注解。@Repository:用于对Dao实现类进行标注。@Service:用于对Service实现类进行标注。@controller:用于对controller实现类进行标注。2、自动装配(1)@Autowired:通过@Autowired注解实现Bean的依赖注入。(2)@Autowired的required属性:用来指定是否必须找到匹配的Bean。(3)@Qualifier,指定Bean的名称。profile:用于切换开发环境。Spring EL:概念:更为灵活的注入方式,能够在运行时构建复杂表达式,存取对象属性、对象方法调用等。作用:1、使用bean id引用bean。2、调用指定对象的方法和访问对象的属性。3、进行运算。4、提供正则表达式进行匹配。5、集合配置。(二)面向切面编程(Aspect Oriented Programming)概述:AOP技术利用"横切"技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。AOP相关概念1、方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的 Advisor或拦截器实现。2、连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。3、通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。Spring中定义了四个advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice。4、切入点(Pointcut): 指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。 Spring定义了Pointcut接口,用来组合MethodMatcher和ClassFilter,可以通过名字很清楚的理解, MethodMatcher是用来检查目标类的方法是否可以被应用此通知,而ClassFilter是用来检查Pointcut是否应该应用到目标类上。5、引入(Introduction): 添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口。6、目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO。7、AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。8、织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。前置通知(Before advice): 在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。后置通知(After advice): 当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。环绕通知(Around Advice): 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。返回后通知(After returning advice): 在某连接点(join point)正常完成后执行的通知,例如,一个方法没有抛出任何异常,正常返回。抛出异常后通知(After throwing advice): 在方法抛出异常退出时执行的通知。Spring AOP实现的四种方式:1、使用proxyFactoryBean和对应的接口实现AOP2、使用XML配置AOP3、使用@AspectJ注解驱动切面4、使用AspectJ注入切面多切面的情况:(1)aspect里面有一个order属性,order属性的数字就是横切关注点的顺序。(2)Spring默认以aspect的定义顺序作为织入顺序。(三)Spring事务管理1、事务管理器Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器。2、事务属性的定义(1)传播行为:Spring定义了七种传播行为,以下为常见类型:PROPAGATION_REQUIRED:表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务PROPAGATION_SUPPORTS:表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行PROPAGATION_MANDATORY:表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常(2)隔离级别隔离级别定义了一个事务可能受其他并发事务影响的程度。ISOLATION_DEFAULT:使用后端数据库默认的隔离级别ISOLATIONREADUNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读.ISOLATIONREADCOMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATIONREPEATABLEREAD:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的(3)只读:通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。(4)事务超时:事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。(5)回滚规则:这些规则定义了哪些异常会导致事务回滚而哪些不会。3、编程式和声明式事务的区别Spring提供了对编程式事务和声明式事务的支持,编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于AOP)有助于用户将操作与事务规则进行解耦。简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。宜信技术学院 作者:姚远

February 27, 2019 · 1 min · jiezi

Spring Cloud Alibaba迁移指南(二):零代码替换 Eureka

自 Spring Cloud 官方宣布 Spring Cloud Netflix 进入维护状态后,我们开始制作《Spring Cloud Alibaba迁移指南》系列文章,向开发者提供更多的技术选型方案,并降低迁移过程中的技术难度。第二篇,Spring Cloud Alibaba 实现了 Spring Cloud 服务注册的标准规范,这就天然的给开发者提供了一种非常便利的方式将服务注册中心的 Eureka 迁移到开源的 Nacos 。 第一篇回顾:一行代码从 Hystrix 迁移到 Sentinel零代码使用 Nacos 替换 Eureka如果你需要使用 Spring Cloud Alibaba 的开源组件 spring-cloud-starter-alibaba-nacos-discovery 来替换 Eureka。需要完成以下几个简单的步骤即可。1. __本地需要安装 Nacos。__Nacos 的安装方式也是极其的简单,参考 Nacos 官网。假设现在已经正常启动了 Nacos 。2. 添加 Nacos 的 pom 依赖,同时去掉 Eureka。 在需要替换的工程目录下找到 maven 的配置文件 pom.xml。添加如下的 pom 依赖:<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>0.2.1.RELEASE</version> </dependency></dependencies>同时将依赖的 spring-cloud-starter-netflix-eureka-client pom 给去掉。 3. application.properties 配置。 一些关于 Nacos 基本的配置也必须在 application.properties(也可以是application.yaml)配置,如下所示: application.properties:spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848同时将和 Eureka 相关的配置删除。4. (可选) 更换 EnableEurekaClient 注解。如果在你的应用启动程序类加了 EnableEurekaClient 注解,这个时候需要更符合 Spring Cloud 规范的一个注解EnableDiscoveryClient 。直接启动你的应用即可。到目前为止,就已经完成了 “零行代码使用 Nacos 替换 Eureka”。完整的方式可参考 Spring Cloud Alibaba 的官方 Wiki 文档。零代码使用 ANS 替换 Eureka如果你需要使用 Spring Cloud Alibaba 的商业化组件 spring-cloud-starter-alicloud-ans 来替换 Eureka。也是仅需完成以下几个简单的步骤即可。1. 本地需要安装 轻量版配置中心。 轻量版配置中心的下载和启动方式可参考 这里。假设现在已经正常启动了轻量版配置中心 。2. 添加 ANS 的 pom 依赖,同时去掉 Eureka。 在需要替换的工程目录下找到 maven 的配置文件 pom.xml。添加如下的 pom 依赖:<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-ans</artifactId> <version>0.2.1.RELEASE</version> </dependency></dependencies>同时将依赖的 org.springframework.cloud:spring-cloud-starter-netflix-eureka-client pom 给去掉。 3. (可选) application.properties 配置。 一些关于 ANS 基本的配置也可以在 application.properties(也可以是application.yaml)配置,如下所示: application.properties:spring.cloud.alicloud.ans.server-list=127.0.0.1spring.cloud.alicloud.ans.server-port=8080如果不配置的话,默认值就是 127.0.0.1 和 8080 ,因此这一步是可选的。同时将和 Eureka 相关的配置删除。4. (可选) 更换 EnableEurekaClient 注解。如果在你的应用启动程序类加了 EnableEurekaClient 注解,这个时候需要更符合 Spring Cloud 规范的一个注解EnableDiscoveryClient 。代码层面不需要改动任何代码,直接启动你的应用即可。到目前为止,就已经完成了 “零代码使用 ANS 替换 Eureka”。完整的使用方式可参考 Spring Cloud Alibaba 的官方 Wiki 文档。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 27, 2019 · 1 min · jiezi

关于 synchronizeOnSession

本文为[原创]文章,转载请标明出处。原文链接:https://weyunx.com/2019/01/22…原文出自微云的技术博客最近在维护一个老项目,发现了一个问题。我们新增了一个耗时较久的复杂查询的功能,页面采用了 ajax 异步请求数据,但是请求未返回之前,点击页面其他功能都只能打开空白页,必须等待之前的数据返回后才能开始加载,整个过程是串行等待,调试过程中发现服务器仅分配了一个线程给该用户。故查看了一下原始代码,发现 web.xml 中配置了如下参数:<init-param> <param-name>synchronizeOnSession</param-name> <param-value>true</param-value></init-param>看了一下 spring mvc 的说明文档,仅找到一处说明:Enforces the presence of a session. As a consequence, such an argument is never null. Note that session access is not thread-safe. Consider setting the RequestMappingHandlerAdapter instance’s synchronizeOnSession flag to true if multiple requests are allowed to concurrently access a session.因为 session 是非线程安全的,如果需要保证用户能够在多次请求中正确的访问同一个 session ,就要将 synchronizeOnSession 设置为 TRUE 。所以此处把synchronizeOnSession 改为 false 后,问题随之解决,调试中可以看到服务器为用户分配了多个线程。同时也可以参考这个例子参考资料docs.spring.io

February 27, 2019 · 1 min · jiezi

Fundebug后端Java异常监控插件更新至0.3.1,修复Maven下载失败的问题

摘要: 0.3.1修复Maven下载失败的问题。监控Java应用1. pom.xml 配置fundebug-java依赖<dependency> <groupId>com.fundebug</groupId> <artifactId>fundebug-java</artifactId> <version>0.3.1</version></dependency>2. 在项目中引入 fundebug 并配置 apikeyimport com.fundebug.Fundebug;Fundebug fundebug = new Fundebug(“apikey”);注意:获取apikey需要免费注册帐号并且创建项目。可以参考 Demo 项目Fundebug/fundebug-java-demo。监控Spring应用1. pom.xml配置fundebug-spring依赖<dependency> <groupId>com.fundebug</groupId> <artifactId>fundebug-spring</artifactId> <version>0.3.1</version></dependency>2. 在项目中引入fundebug并配置apikey新增FundebugConfig.javaimport org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import com.fundebug.Fundebug;import com.fundebug.SpringConfig;@Configuration@Import(SpringConfig.class)public class FundebugConfig { @Bean public Fundebug getBean() { return new Fundebug(“apikey”); }}注意:获取apikey需要免费注册帐号并且创建项目。可以参考Demo项目Fundebug/fundebug-spring-demo。参考Fundebug文档 - JavaMaven入门教程关于FundebugFundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了9亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!版权声明转载时请注明作者Fundebug以及本文地址:https://blog.fundebug.com/2019/01/07/fundebug-java-0-2-0/

February 27, 2019 · 1 min · jiezi

spring + angular 实现导出excel

需求描述要求批量导出数据,以excel的格式。选择方式前台 + 后台之前在别的项目中也遇到过导出的问题,解决方式是直接在前台导出将表格导出。这次没有选择前台导出的方式,是由于需要导出所有的数据,所以考虑直接在后台获取所有的数据,然后就直接导出,最后前台触发导出API。后台实现导出使用的是POI,在上一篇文章中,我已做了基本的介绍,这里就不做介绍配置了,参照:POI实现将导入Excel文件创建表格首先先建立一张表,这里要建立.xlsx格式的表格,使用XSSFWorkbook:Workbook workbook = new XSSFWorkbook();Sheet sheet = workbook.createSheet(“new sheet”);接着创建表格的行和单元格:Row row = sheet.createRow(0);row.createCell(0);然后设置表头:row.createCell(0).setCellValue(“学号”);row.createCell(1).setCellValue(“姓名”);row.createCell(2).setCellValue(“手机号码”);最后获取所有的数据,对应的填写到单元格中:int i = 1;for (Student student : studentList) { row = sheet.createRow(i); row.createCell(0).setCellValue(student.getStudentNumber()); row.createCell(1).setCellValue(student.getName()); row.createCell(2).setCellValue(student.getPhoneNumber()); i++;}输出这部分是纠结比较久的,反复试了很多次。一开始是直接以文件输出流的形式输出的:FileOutputStream output = new FileOutputStream(“test.xlsx”);workbook.write(output);这样可以正确生成文件,但是问题是,它会生成在项目的根目录下。而我们想要的效果是,下载在本地自己的文件夹中。要解决这个问题,需要添加相应信息,返回给浏览器:OutputStream fos = response.getOutputStream();response.reset();String fileName = “test”;fileName = URLEncoder.encode(fileName, “utf8”);response.setHeader(“Content-disposition”, “attachment;filename="+ fileName+".xlsx”);response.setCharacterEncoding(“UTF-8”);response.setContentType(“application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”);workbook.write(fos);fos.close();后台完成代码:public void batchExport(HttpServletResponse response) { logger.debug(“创建工作表”); Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet(“new sheet”); logger.debug(“获取所有学生”); List<Student> studentList = (List<Student>) studentRepository.findAll(); logger.debug(“建立表头”); Row row = sheet.createRow(0); row.createCell(0).setCellValue(“学号”); row.createCell(1).setCellValue(“姓名”); row.createCell(2).setCellValue(“手机号码”); logger.debug(“将学生信息写入对应单元格”); int i = 1; for (Student student : studentList) { row = sheet.createRow(i); row.createCell(0).setCellValue(student.getStudentNumber()); row.createCell(1).setCellValue(student.getName()); row.createCell(2).setCellValue(student.getPhoneNumber()); i++; } OutputStream fos; try { fos = response.getOutputStream(); response.reset(); String fileName = “test”; fileName = URLEncoder.encode(fileName, “utf8”); response.setHeader(“Content-disposition”, “attachment;filename="+ fileName+".xlsx”); response.setCharacterEncoding(“UTF-8”); response.setContentType(“application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”);// 设置contentType为excel格式 workbook.write(fos); fos.close(); } catch (Exception e) { e.printStackTrace(); }}前台实现在前台调用的时候,也经历了多次失败,google了很多篇文章,各种各样的写法都有,自己也是试了试,前台后台都对照做了很多尝试,但基本都是有问题的。这里我值给出我最后选择配套后台的方法。// 后台导出路由const exportUrl = ‘/api/student/batchExport’;// 创建a标签,并点击let a = document.createElement(‘a’);document.body.appendChild(a);a.setAttribute(‘style’, ‘display:none’);a.setAttribute(‘href’, exportUrl);a.click();URL.revokeObjectURL(exportUrl);最后的实现还是一种比较简单的方法,创建了一个a标签,然后隐式点击。注意到这里我没有使用http请求,主要是他并不能触发浏览器的下载,在发起请求后,并没有正确的生成文件,具体是什么还不清楚。后面弄明白后我会再更新这篇文章。总结我们在google的时候,很多时候,我们并不能一下子就找到我们想要的东西,但是并不是说这在做无用功,因为我们往往会在一些类似的文章中找到灵感。所以,当我们没有直接找到我们想要的结果的时候,不妨大胆的做一些尝试,因为我们会在一次又一次失败的尝试中,慢慢的了解问题的原理到底是怎么回事。相关参考:https://my.oschina.net/u/3644…https://blog.csdn.net/LUNG108… ...

February 26, 2019 · 1 min · jiezi

Spring Cloud Gateway 使用 Token 验证

引入依赖<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement><dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency></dependencies>自定义过滤器可以继承 AbstractGatewayFilterFactory 或实现 GlobalFilter 实现过滤请求功能GatewayFilterGatewayFilter 只能指定路径上应用import org.springframework.cloud.gateway.filter.GatewayFilter;import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;import org.springframework.http.HttpStatus;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;@Componentpublic class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> { public AuthGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { System.out.println(“Welcome to AuthFilter.”); String token = exchange.getRequest().getHeaders().getFirst(“sign”); if (Config.secret.equals(token)) { return chain.filter(exchange); } ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); }; } static class Config { static String secret = “1234”; }}spring: cloud: gateway: routes: - id: service2_route uri: http://127.0.0.1:8082 predicates: - Path=/s2/** filters: - StripPrefix=1 # 去掉路径的 n 个前缀 - Auth=true # 输入过滤器类的名称前缀GlobalFilterGlobalFilter 可以在全局应用import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.http.HttpStatus;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;@Componentpublic class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println(“Welcome to AuthGlobalFilter.”); ServerHttpRequest request = exchange.getRequest(); String sign = request.getHeaders().get(“sign”).get(0); String token = “1234”; if(token.equals(sign)) { return chain.filter(exchange); } ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } @Override public int getOrder() { return 0; }} ...

February 26, 2019 · 1 min · jiezi

Spring AOP(三) Advisor类架构

Spring AOP是Spring的两大基石之一,不了解其基础概念的同学可以查看这两篇文章AOP基本概念和修饰者模式和JDK Proxy。 如果从代码执行角度来看,Spring AOP的执行过程分为四大步骤:步骤一:Spring框架生成Advisor实例,可以是@Aspect,@Async等注解生成的实例,也可以是程序员自定义的AbstractAdvisor子类的实例。步骤二:Spring框架在目标实例初始化完成后,也就是使用BeanPostProcessor的postProcessAfterInitialization方法,根据Advisor实例中切入点Pointcut的定义,选择出适合该目标对象的Advisor实例。步骤三:Spring框架根据Advisor实例生成代理对象。步骤四:调用方法执行过程时,Spring框架执行Advisor实例的通知Advice逻辑。 由于这四个步骤涉及的源码量较大,一篇文章无法直接完全讲解完,本篇文章只讲解第一步Advisor实例生成的源码分析。接下来的文章我们就依次讲解一下后续步骤中比较关键的逻辑。Advisor类架构 Spring中有大量的机制都是通过AOP实现的,比如说@Async的异步调用和@Transational。此外,用户也可以使用@Aspect注解定义切面或者直接继承AbstractPointcutAdvisor来提供切面逻辑。上述这些情况下,AOP都会生成对应的Advisor实例。 我们先来看一下Advisor的相关类图。首先看一下org.aopalliance包下的类图。aopalliance是AOP组织下的公用包,用于AOP中方法增强和调用,相当于一个jsr标准,只有接口和异常,在AspectJ、Spring等AOP框架中使用。 aopalliance定义了AOP的通知Advice和连接点Joinpoint接口,并且还有继承上述接口的MethodInterceptor和MethodInvocation。这两个类相信大家都很熟悉。 然后我们来看一下Spring AOP中Advisor相关的类图。Advisor是Spring AOP独有的概念,比较重要的类有AbstractPointcutAdvisor和InstantiationModelAwarePointcutAdvisor。相关的讲解都在图中表明了,如果这张图中的概念和类同学们都熟识,那么对AOP的了解就已经很深入了。获取所有Advisor实例 AOP生成Advisor实例的函数入口是AbstractAdvisorAutoProxyCreator的findCandidateAdvisors函数。// AbstractAdvisorAutoProxyCreator.java 找出当前所有的Advisorprotected List<Advisor> findCandidateAdvisors() { Assert.state(this.advisorRetrievalHelper != null, “No BeanFactoryAdvisorRetrievalHelper available”); return this.advisorRetrievalHelper.findAdvisorBeans();}// AnnotationAwareAspectJAutoProxyCreator,是AbstractAdvisorAutoProxyCreator的子类@Overrideprotected List<Advisor> findCandidateAdvisors() { // 调用父类的findCandidateAdvisor函数,一般找出普通的直接 // 继承Advisor接口的实例,比如说@Async所需的AsyncAnnotationAdvisor List<Advisor> advisors = super.findCandidateAdvisors(); // 为AspectJ的切面构造Advisor,也就是说处理@Aspect修饰的类,生成上文中说的InstantiationModelAwarePointcutAdvisor实例 if (this.aspectJAdvisorsBuilder != null) { advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); } return advisors;} 相关的ProxyCreator也有一个类体系,不过太过繁杂,而且重要性不大,我们就先略过,直接将具体的类。由上边代码可知AbstractAdvisorAutoProxyCreator 的findCandidateAdvisors 函数是直接获取Spring容器中的Advisor实例,比如说AsyncAnnotationAdvisor实例,或者说我们自定义的AbstractPointcutAdvisor的子类实例。AdvisorRetrievalHelper 的findAdvisorBeans 函数通过BeanFactory的getBean获取了所有类型为Advisor的实例。 而AnnotationAwareAspectJAutoProxyCreator 看其类名就可知,是与AspectJ相关的创建器,用来获取@Aspect定义的Advisor实例,也就是InstantiationModelAwarePointcutAdvisor实例。 接下去我们看一下BeanFactoryAspectJAdvisorsBuilder的buildAspectJAdvisors函数,它根据@Aspect修饰的切面实例生成对应的Advisor实例。public List<Advisor> buildAspectJAdvisors() { List<String> aspectNames = this.aspectBeanNames; // 第一次初始化,synchronized加双次判断,和经典单例模式的写法一样。 if (aspectNames == null) { synchronized (this) { aspectNames = this.aspectBeanNames; if (aspectNames == null) { // Spring源码并没有buildAspectJAdvisorsFirstly函数,为了方便理解添加。 // 获取aspectNames,创建Advisor实例,并且存入aspectFactoryCache缓存 return buildAspectJAdvisorsFirstly(); } } } if (aspectNames.isEmpty()) { return Collections.emptyList(); } List<Advisor> advisors = new ArrayList<>(); // 遍历aspectNames,依次获取对应的Advisor实例,或者是MetadataAwareAspectInstanceFactory生成的Advisor实例 for (String aspectName : aspectNames) { List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName); // cache可以取到实例,该Advisor是单例的 if (cachedAdvisors != null) { advisors.addAll(cachedAdvisors); } else { // 取得Advisor对应的工厂类实例,再次生成Advisor实例,该Advisor是多实例的。 MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } return advisors;} buildAspectJAdvisors函数执行时分为两种情况,第一个未初始化时,也就是aspectNames为null时,执行buildAspectJAdvisorsFirstly进行第一次初始化,在这一过程中生成切面名称列表aspectBeanNames和要返回的Advisor 列表,并且将生成的Advisor实例放置到advisorsCache中。 第二种情况则是已经初始化后再次调用,遍历aspectNames,从advisorsCache 取出对应的Advisor实例,或者从advisorsCache取出Advisor对应的工厂类对象,再次生成Advisor实例。public List<Advisor> buildAspectJAdvisorsFirstly() { List<Advisor> advisors = new ArrayList<>(); List<String> aspectNames = new ArrayList<>(); // 调用BeanFactoryUtils获取所有bean的名称 String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Object.class, true, false); for (String beanName : beanNames) { if (!isEligibleBean(beanName)) { continue; } // 获取对应名称的bean实例 Class<?> beanType = this.beanFactory.getType(beanName); if (beanType == null) { continue; } /** * AbstractAspectJAdvisorFactory类的isAspect函数来判断是否为切面实例 * 判断条件为是否被@Aspect修饰或者是由AspectJ编程而来。 */ if (this.advisorFactory.isAspect(beanType)) { aspectNames.add(beanName); AspectMetadata amd = new AspectMetadata(beanType, beanName); // 切面的属性为单例模式 if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); // 获取一个切面中所有定义的Advisor实例。一个切面可以定义多个Advisor。 List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory); // 单例模式,只需要将生成的Advisor添加到缓存 if (this.beanFactory.isSingleton(beanName)) { this.advisorsCache.put(beanName, classAdvisors); } // 多实例模式,需要保存工厂类,便于下一次再次生成Advisor实例。 else { this.aspectFactoryCache.put(beanName, factory); } advisors.addAll(classAdvisors); } else { MetadataAwareAspectInstanceFactory factory = new PrototypeAspectInstanceFactory(this.beanFactory, beanName); this.aspectFactoryCache.put(beanName, factory); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } } this.aspectBeanNames = aspectNames; return advisors;} buildAspectJAdvisorsFirstly函数的逻辑如下:首先使用BeanFactoryUtils获取了BeanFactory中所有的BeanName,然后进而使用BeanFactory获取所有的Bean实例。遍历Bean实例,通过ReflectiveAspectJAdvisorFactory的isAspect函数判断该实例是否为切面实例,也就是被@Aspect注解修饰的实例。如果是,则使用ReflectiveAspectJAdvisorFactory,根据切面实例的定义来生成对应的多个Advisor实例,并且将其加入到advisorsCache中。生成InstantiationModelAwarePointcutAdvisorImpl实例 ReflectiveAspectJAdvisorFactory 的getAdvisors 函数会获取@Aspect修饰的实例中所有没有被@Pointcut修饰的方法,然后调用getAdvisor函数,并且将这些方法作为参数。public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) { validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); // 获得该方法上的切入点条件表达式 AspectJExpressionPointcut expressionPointcut = getPointcut( candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass()); if (expressionPointcut == null) { return null; } // 生成Advisor实例 return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName);}private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) { // 获得该函数上@Pointcut, @Around, @Before, @After, @AfterReturning, @AfterThrowing注解的信息 AspectJAnnotation<?> aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); // 没有上述注解,则直接返回 if (aspectJAnnotation == null) { return null; } AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]); // 获得注解信息中的切入点判断表达式 ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); if (this.beanFactory != null) { ajexp.setBeanFactory(this.beanFactory); } return ajexp;} getAdvisor函数就是根据作为参数传入的切面实例的方法上的注解来生成Advisor实例,也就是InstantiationModelAwarePointcutAdvisorImpl对象。依据方法上的切入点表达式生成AspectJExpressionPointcut 。 我们都知道PointcutAdvisor实例中必然有一个Pointcut和Advice实例。修饰在方法上的注解包括:@Pointcut, @Around, @Before, @After, @AfterReturning和@AfterThrowing,所以InstantiationModelAwarePointcutAdvisorImpl会依据不同的不同的注解生成不同的Advice通知。public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut, Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { // …. 省略成员变量的直接赋值 // 单例模式时 this.pointcut = this.declaredPointcut; this.lazy = false; // 按照注解解析 Advice this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);} InstantiationModelAwarePointcutAdvisorImpl的构造函数中会生成对应的Pointcut和Advice。instantiateAdvice函数调用了ReflectiveAspectJAdvisorFactory的getAdvice函数。// ReflectiveAspectJAdvisorFactorypublic Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); validate(candidateAspectClass); // 获取 Advice 注解 AspectJAnnotation<?> aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); if (aspectJAnnotation == null) { return null; } // 检查是否为AspectJ注解 if (!isAspect(candidateAspectClass)) { throw new AopConfigException(“Advice must be declared inside an aspect type: " + “Offending method ‘” + candidateAdviceMethod + “’ in class [” + candidateAspectClass.getName() + “]”); } AbstractAspectJAdvice springAdvice; // 按照注解类型生成相应的 Advice 实现类 switch (aspectJAnnotation.getAnnotationType()) { case AtPointcut: if (logger.isDebugEnabled()) { logger.debug(“Processing pointcut ‘” + candidateAdviceMethod.getName() + “’”); } return null; case AtAround: // @Before 生成 AspectJMethodBeforeAdvice springAdvice = new AspectJAroundAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; case AtBefore: // @After 生成 AspectJAfterAdvice springAdvice = new AspectJMethodBeforeAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; case AtAfter: // @AfterReturning 生成 AspectJAfterAdvice springAdvice = new AspectJAfterAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; case AtAfterReturning: // @AfterThrowing 生成 AspectJAfterThrowingAdvice springAdvice = new AspectJAfterReturningAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); if (StringUtils.hasText(afterReturningAnnotation.returning())) { springAdvice.setReturningName(afterReturningAnnotation.returning()); } break; case AtAfterThrowing: // @Around 生成 AspectJAroundAdvice springAdvice = new AspectJAfterThrowingAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); } break; default: throw new UnsupportedOperationException( “Unsupported advice type on method: " + candidateAdviceMethod); } // 配置Advice springAdvice.setAspectName(aspectName); springAdvice.setDeclarationOrder(declarationOrder); // 获取方法的参数列表方法 String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); if (argNames != null) { // 设置参数名称 springAdvice.setArgumentNamesFromStringArray(argNames); } springAdvice.calculateArgumentBindings(); return springAdvice;} 至此,Spring AOP就获取了容器中所有的Advisor实例,下一步在每个实例初始化完成后,根据这些Advisor的Pointcut切入点进行筛选,获取合适的Advisor实例,并生成代理实例。后记 Spring AOP后续文章很快就会更新,请大家继续关注。 ...

February 25, 2019 · 3 min · jiezi

Spring MVC实现Spring Security,Spring Stomp websocket Jetty嵌入式运行

使用Spring框架各个组件实现一个在线聊天网页,当有用户连接WebSocket,服务器监听到用户连接会使用Stomp推送最新用户列表,有用户断开刷新在线列表,实时推送用户聊天信息。引入Jetty服务器,直接嵌入整个工程可以脱离Java Web容器独立运行,使用插件打包成一个jar文件,就像Spring Boot一样运行,部署。pom.xml 依赖 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <jetty.version>9.4.8.v20171121</jetty.version> <spring.version>5.0.4.RELEASE</spring.version> <jackson.version>2.9.4</jackson.version> <lombok.version>1.16.18</lombok.version> <dbh2.version>1.4.196</dbh2.version> <jcl.slf4j.version>1.7.25</jcl.slf4j.version> <spring.security.version>5.0.3.RELEASE</spring.security.version> <logback.version>1.2.3</logback.version> <activemq.version>5.15.0</activemq.version> </properties> <dependencies> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> <version>${jetty.version}</version> </dependency> <!– 添加websocket 依赖不然会出现 java.lang.IllegalStateException: No suitable default RequestUpgradeStrategy found –> <dependency> <groupId>org.eclipse.jetty.websocket</groupId> <artifactId>websocket-server</artifactId> <version>${jetty.version}</version> </dependency> <dependency> <groupId>org.eclipse.jetty.websocket</groupId> <artifactId>websocket-api</artifactId> <version>${jetty.version}</version> </dependency> <!–spring mvc –> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webflux</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>${spring.version}</version> </dependency> <!–spring security –> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring.security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring.security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> <version>${spring.security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> <version>${spring.security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-messaging</artifactId> <version>${spring.security.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>${dbh2.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${jcl.slf4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-broker</artifactId> <version>${activemq.version}</version> </dependency> <dependency> <groupId>io.projectreactor.ipc</groupId> <artifactId>reactor-netty</artifactId> <version>0.7.2.RELEASE</version> </dependency> </dependencies>1. 配置H2 嵌入式数据库 @Bean //内存模式 public DataSource dataSource(){ EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); EmbeddedDatabase build = builder.setType(EmbeddedDatabaseType.H2) .addScript(“db/sql/create-db.sql”) //每次创建数据源都会执行脚本 .addScript(“db/sql/insert-data.sql”) .build(); return build; }这种方式是利用Spring 内置的嵌入式数据库的数据源模板,创建的数据源,比较简单,但是这种方式不支持定制,数据只能保存在内存中,项目重启数据就会丢失了。设置数据保存到硬盘 @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(“org.h2.Driver”); dataSource.setUsername(“embedded”); dataSource.setPassword(“embedded”); dataSource.setUrl(“jdbc:h2:file:./data;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;”); return dataSource; }如果你还想每次创建数据源执行初始化sql,使用org.springframework.jdbc.datasource.init.ResourceDatabasePopulator 装载sql 脚本用于初始化或清理数据库 @Bean public ResourceDatabasePopulator databasePopulator() { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.addScript(schema); populator.addScripts(data); populator.setContinueOnError(true); return populator; }设置DatabasePopulator 对象,用户数据源启动或者消耗的时候执行脚本 @Bean public DataSourceInitializer initializer() { DataSourceInitializer initializer = new DataSourceInitializer(); initializer.setDatabasePopulator(databasePopulator()); initializer.setDataSource(dataSource()); return initializer; }启用H2 web Console @Bean(initMethod = “start”,destroyMethod = “stop”) public Server DatasourcesManager() throws SQLException { return Server.createWebServer("-web","-webAllowOthers","-webPort",“8082”); }浏览器打开 http://localhost:8082 访问H2 控制台设置事务管理器 @Bean public PlatformTransactionManager transactionManager() { PlatformTransactionManager manager = new DataSourceTransactionManager(dataSource()); return manager; }}到这里,嵌入H2数据库配置基本已经设置完成了2. Spring MVC配置import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.List;@Configuration@EnableWebMvc@ComponentScan(basePackages = “org.ting.spring.controller”, //基包路径设置 includeFilters = @ComponentScan.Filter(value = {ControllerAdvice.class,Controller.class})) //只扫描MVC controll的注解public class WebMvcConfiguration implements WebMvcConfigurer { public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new MappingJackson2HttpMessageConverter()); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //添加静态路径映射 registry.addResourceHandler("/static/").addResourceLocations(“classpath:/static/”); }}3. Jetty嵌入式服务因为Spring 注解扫描只能注册一个类, 使用@Import引入其他的配置类@Configuration@ComponentScan(basePackages = “org.ting.spring”, excludeFilters = {@ComponentScan.Filter(value = {Controller.class,ControllerAdvice.class})})@Import({WebMvcConfiguration.class}) //引入Spring MVC配置类public class WebRootConfiguration { @Autowired private DataSource dataSource; @Bean public JdbcTemplate jdbcTemplate(){ JdbcTemplate template = new JdbcTemplate(dataSource); return template; }}使用Spring AnnotationConfigWebApplicationContext 启动注解扫描,注册创建bean将WebApplicationContext,在将对象传给DispatcherServletpublic class JettyEmbedServer { private final static int DEFAULT_PORT = 9999; private final static String DEFAULT_CONTEXT_PATH = “/”; private final static String MAPPING_URL = “/*”; public static void main(String[] args) throws Exception { Server server = new Server(DEFAULT_PORT); JettyEmbedServer helloServer = new JettyEmbedServer(); server.setHandler(helloServer.servletContextHandler()); server.start(); server.join(); } private ServletContextHandler servletContextHandler() { WebApplicationContext context = webApplicationContext(); ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); servletContextHandler.setContextPath(DEFAULT_CONTEXT_PATH); ServletHolder servletHolder = new ServletHolder(new DispatcherServlet(context)); servletHolder.setAsyncSupported(true); servletContextHandler.addServlet(servletHolder, MAPPING_URL); return servletContextHandler; } private WebApplicationContext webApplicationContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(WebRootConfiguration.class); return context; } 3. 配置Spring Security默认Spring Security拦截请求,登录失败,登录成功都是页面跳转的方式,我们希望ajax请求的时候,无论是被拦截了,或者登录失败,成功都可以返回json格式数据,由前端人员来处理。 根据HttpRequestServlet 请求头 X-Requested-With是否等于XMLHttpRequest 判断是否是ajax。public class RespnonseJson { public static void jsonType(HttpServletResponse response) { response.setContentType(“application/json;charset=UTF-8”); response.setCharacterEncoding(“utf-8”); } public static boolean ajaxRequest(HttpServletRequest request){ String header = request.getHeader(“X-Requested-With”); return ! StringUtils.isEmpty(header) && header.equals(“XMLHttpRequest”); } public static boolean matchURL(String url) { Pattern compile = Pattern.compile("^/api/.+"); return compile.matcher(url).matches(); }}登录认证处理器public class RestAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint { / * @param loginFormUrl URL where the login page can be found. Should either be * relative to the web-app context path (include a leading {@code /}) or an absolute * URL. */ public RestAuthenticationEntryPoint(String loginFormUrl) { super(loginFormUrl); } @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String uri = request.getRequestURI(); if (matchURL(uri)) { // /api 都是ajax 请求 jsonType(response); response.getWriter().println(getErr(authException.getMessage())); }else if (ajaxRequest(request)){ jsonType(response); response.getWriter().println(getErr(authException.getMessage())); }else super.commence(request,response,authException); } private String getErr(String description) throws JsonProcessingException { Result result = Result.error(Result.HTTP_FORBIDDEN, description); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(result); }}登录成功处理public class RestAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String uri = request.getRequestURI(); if (matchURL(uri)){ jsonType(response); String value = loginSuccess(); response.getWriter().println(value); }else if (ajaxRequest(request)){ jsonType(response); String success = loginSuccess(); response.getWriter().println(success); }else super.onAuthenticationSuccess(request,response,authentication); } private String loginSuccess() throws JsonProcessingException { Result success = Result.success(“sign on success go to next!”); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(success); }}登录失败处理public class RestAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if (ajaxRequest(request)){ jsonType(response); String err = getErr(exception.getMessage()); response.getWriter().println(err); }else super.onAuthenticationFailure(request,response,exception); } public String getErr(String description) throws JsonProcessingException { Result result = Result.error(Result.HTTP_AUTH_FAILURE, description); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(result); }}我在网上搜索ajax 认证错误,很多博客是这样写的response.sendError(500, “Authentication failed”);这个错误会被Jetty 错误页面捕获,扰乱返回JSON数据,这个细节要注意下注册Handler @Bean public AuthenticationEntryPoint entryPoint() { RestAuthenticationEntryPoint entryPoint = new RestAuthenticationEntryPoint("/static/html/login.html"); return entryPoint; } @Bean public SimpleUrlAuthenticationSuccessHandler successHandler() { RestAuthSuccessHandler successHandler = new RestAuthSuccessHandler(); return successHandler; } @Bean public SimpleUrlAuthenticationFailureHandler failureHandler() { RestAuthFailureHandler failureHandler = new RestAuthFailureHandler(); return failureHandler; }配置url 认证 @Bean public SessionRegistry sessionManager() { return new SessionRegistryImpl(); } @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(entryPoint()) .and() .authorizeRequests() .antMatchers("/static/html/jetty-chat.html", “/api/user/online”, “/api/user/loginuser”) .authenticated() //设置需要认证才可以请求的接口 .and() .formLogin() .successHandler(successHandler()) //登录成功处理 .failureHandler(failureHandler()) //登录失败处理 .loginPage("/static/html/login.html") //登录页面 .loginProcessingUrl("/auth/login") //登录表单url .defaultSuccessUrl("/static/html/jetty-chat.html") //成功跳转url .permitAll() .and().csrf().disable()//禁用csrf 因为没有使用模板引擎 .sessionManagement().maximumSessions(1) //设置同一个账户,同时在线次数 .sessionRegistry(sessionManager()) // 设置Session 管理器, .expiredUrl("/static/html/login.html") //session 失效后,跳转url .maxSessionsPreventsLogin(false) //设置true,达到session 最大登录次数后,后面的账户都会登录失败,false 顶号 前面登录账户会被后面顶下线 ; //注销账户,跳转到登录页面 http.logout().logoutUrl("/logout").logoutSuccessUrl("/static/html/login.html");在配置类添加@EnableWebSecurity,在扫描类上引入Spring Security配置,大功告成了,并没有!Spring Security 是使用Filter来处理一些认证请求,需要我们在Jetty中手动注册拦截器 //手动注册拦截器,让Spring Security 生效 FilterHolder filterHolder = new FilterHolder(new DelegatingFilterProxy(“springSecurityFilterChain”)); servletContextHandler.addFilter(filterHolder, MAPPING_URL, null); servletContextHandler.addEventListener(new ContextLoaderListener(context)); servletContextHandler.addEventListener(new HttpSessionEventPublisher()); //使用security session 监听器 限制只允许一个用户登录4. 配置WebSocketStompConfig@Configuration@EnableWebSocketMessageBroker@ComponentScan(basePackages = “org.ting.spring.stomp.message”)@Slf4jpublic class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer { //设置连接的端点路径 @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(“endpoint”).withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息 registry.enableSimpleBroker("/topic", “/queue”); // 定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀 registry.setApplicationDestinationPrefixes("/app"); //使用客户端一对一通信 registry.setUserDestinationPrefix("/user"); registry.setPathMatcher(new AntPathMatcher(".")); }}配置stomp 频道认证@Configurationpublic class SocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages.simpDestMatchers("/user/**").authenticated()//认证所有user 链接 .anyMessage().permitAll(); } //允许跨域 不然会出现 Could not verify the provided CSRF token because your session was not found 异常 @Override protected boolean sameOriginDisabled() { return true; }}信息处理@Controller@Slf4jpublic class StompController { @Autowired private SimpMessagingTemplate messagingTemplate; @MessageExceptionHandler @SendToUser("/queue.errors") public String handleException(Throwable exception) { return exception.getMessage(); } @MessageMapping(“receive.messgae”) public void forwardMsg(ChatMessage message){ log.info(“message : {}",message); message.setLocalDateTime(LocalDateTime.now()); messagingTemplate.convertAndSendToUser(message.getTargetUser().getEmail() ,“queue.notification”,message); }}@MessageMapping 作用与@RequestMapping 功能差不多用于匹配url更多Spring WebSocket 官方文档查看我们使用一个集合来保存连接上的用户,使用连接,断开监听器来修改集合的列表,并将集合的数据发布到频道上。websocket 断开连接监听器@Component@Slf4jpublic class WebSocketDisconnectListener implements ApplicationListener<SessionDisconnectEvent> { @Autowired private UserService userService; @Autowired private SimpMessagingTemplate messageTemplate; @Override public void onApplicationEvent(SessionDisconnectEvent event) { Principal principal = event.getUser(); log.info(“client sessionId : {} name : {} disconnect ….",event.getSessionId(),principal.getName()); if (principal != null){ //已经认证过的用户 User user = userService.findByEmail(principal.getName()); Online.remove(user); messageTemplate.convertAndSend("/topic/user.list”,Online.onlineUsers()); } }}注册连接websocket 监听器@Component@Slf4jpublic class WebSocketSessionConnectEvent implements ApplicationListener<SessionConnectEvent>{ @Autowired private SimpMessagingTemplate messageTemplate; @Autowired private UserService userService; @Override public void onApplicationEvent(SessionConnectEvent event) { Principal principal = event.getUser(); log.info(“client name: {} connect…..",principal.getName()); if (principal != null){ User user = userService.findByEmail(principal.getName()); Online.add(user); messageTemplate.convertAndSend("/topic/user.list”,Online.onlineUsers()); } }}保存在线列表public class Online { private static Map<String,User> maps = new ConcurrentHashMap<>(); public static void add(User user){ maps.put(user.getEmail(),user); } public static void remove(User user){ maps.remove(user.getEmail()); } public static Collection<User> onlineUsers(){ return maps.values(); }}4. Spring Security OAuth2 Client 配置手动配置ClientRegistrationRepository 设置client-id,client-secret,redirect-uri-template @Bean public ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository(githubClientRegstrationRepository() ,googleClientRegistrionRepository()); } public ClientRegistration githubClientRegstrationRepository(){ return CommonOAuth2Provider.GITHUB.getBuilder(“github”) .clientId(env.getProperty(“registration.github.client-id”)) .clientSecret(env.getProperty(“registration.github.client-secret”)) .redirectUriTemplate(env.getProperty(“registration.github.redirect-uri-template”)) .build(); } public ClientRegistration googleClientRegistrionRepository(){ return CommonOAuth2Provider.GOOGLE.getBuilder(“google”) .clientId(env.getProperty(“registration.google.client-id”)) .clientSecret(env.getProperty(“registration.google.client-secret”)) .redirectUriTemplate(env.getProperty(“registration.google.redirect-uri-template”)) .scope( “profile”, “email”) .build(); } @Bean public OAuth2AuthorizedClientService authorizedClientService() { return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository()); }我们使用github,google OAuth2 授权登录的账户,登录通过后保存起来,则需求继承DefaultOAuth2UserService@Service@Slf4jpublic class CustomOAuth2UserService extends DefaultOAuth2UserService { @Autowired private UserService userService; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User oAuth2User = super.loadUser(userRequest); try { oAuth2User = processOAuth2User(oAuth2User,userRequest); } catch (Exception e) { log.error(“processOAuth2User error {}",e); } return oAuth2User; } private OAuth2User processOAuth2User(OAuth2User oAuth2User,OAuth2UserRequest userRequest) { String clientId = userRequest.getClientRegistration().getRegistrationId(); if (clientId.equalsIgnoreCase(“github”)) { Map<String, Object> map = oAuth2User.getAttributes(); String login = map.get(“login”)+"_oauth_github”; String name = (String) map.get(“name”); String avatarUrl = (String) map.get(“avatar_url”); User user = userService.findByEmail(login); if (user == null) { user = new User(); user.setUsername(name); user.setEmail(login); user.setAvatar(avatarUrl); user.setPassword(“123456”); userService.insert(user); }else { user.setUsername(name); user.setAvatar(avatarUrl); userService.update(user); } return UserPrincipal.create(user, oAuth2User.getAttributes()); }else if (clientId.equalsIgnoreCase(“google”)){ Map<String, Object> result = oAuth2User.getAttributes(); String email = result.get(“email”)+"_oauth_google”; String username = (String) result.get(“name”); String imgUrl = (String) result.get(“picture”); User user = userService.findByEmail(email); if (user == null){ user = new User(); user.setEmail(email); user.setPassword(“123456”); user.setAvatar(imgUrl); user.setUsername(username); userService.insert(user); }else { user.setUsername(username); user.setAvatar(imgUrl); userService.update(user); } return UserPrincipal.create(user,oAuth2User.getAttributes()); } return null; }} 重写UserDetailspublic class UserPrincipal implements OAuth2User,UserDetails { private long id; private String name; private String password; private boolean enable; private Collection<? extends GrantedAuthority> authorities; private Map<String,Object> attributes; UserPrincipal(long id,String name,String password,boolean enable,Collection<? extends GrantedAuthority> authorities){ this.id = id; this.name = name; this.password = password; this.authorities = authorities; this.enable = enable; } public static UserPrincipal create(User user){ return new UserPrincipal(user.getId(),user.getEmail() ,user.getPassword(),user.isEnable(),Arrays.asList(new SimpleGrantedAuthority(“ROLE_USER”))); } public static UserPrincipal create(User user, Map<String, Object> attributes) { UserPrincipal userPrincipal = UserPrincipal.create(user); userPrincipal.attributes = attributes; return userPrincipal; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return name; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.enable; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public Map<String, Object> getAttributes() { return this.attributes; } @Override public String getName() { return String.valueOf(this.id); }}设置Spring Security OAuth2 Client@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomOAuth2UserService customOAuth2UserService; @Override protected void configure(HttpSecurity http) throws Exception { http.oauth2Login() .clientRegistrationRepository(clientRegistrationRepository()) .authorizedClientService(authorizedClientService()) .userInfoEndpoint() .userService(customOAuth2UserService) .and() .defaultSuccessUrl("/static/html/jetty-chat.html");}}默认授权端点,点击后直接重定向到授权服务器的登录页面,Spring 默认是: oauth2/authorization/{clientId} 默认授权成功跳转url: /login/oauth2/code/{clientId}这个项目参考的教程:https://www.baeldung.com/spring-security-5-oauth2-loginhttps://www.callicoder.com/spring-boot-security-oauth2-social-login-part-1/这个教程只展示了一部分的代码,想查看完整的项目代码,可以去github: spring-stomp-security-webflux-embedded-jetty查看 ...

February 25, 2019 · 7 min · jiezi

Spring校验@RequestParams和@PathVariables参数

我们在写Rest API接口时候会用到很多的@RequestParam和@PathVariable进行参数的传递,但是在校验的时候,不像使用@RequestBody那样的直接写在实体类中,我们这篇文章讲解一下如何去校验这些参数。依赖配置要使用Java Validation API,我们必须添加validation-api依赖项:<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version></dependency>通过添加@Validated注解来启用控制器中的@RequestParams和@PathVariables的验证:@RestController@RequestMapping("/")@Validatedpublic class Controller { // …}校验@RequestParam我们将数字作为请求参数传递给控制器方法@GetMapping("/name-for-day")public String getNameOfDayByNumber(@RequestParam Integer dayOfWeek) { // …}我们保证dayOfWeek的值在1到7之间,我们使用@Min和@Max注解@GetMapping("/name-for-day")public String getNameOfDayByNumber(@RequestParam @Min(1) @Max(7) Integer dayOfWeek) { // …}任何与这些条件不匹配的请求都将返回HTTP状态500,并显示默认错误消息。如果我们尝试调用http://localhost:8080/name-for-day?dayOfWeek=24这将返回以下响应信息:There was an unexpected error (type=Internal Server Error, status=500).getNameOfDayByNumber.dayOfWeek: must be less than or equal to 7当然我们也可以在@Min和@Max注解后面加上message参数进行修改默认的返回信息。校验@PathVariable和校验@RequestParam一样,我们可以使用javax.validation.constraints包中的注解来验证@PathVariable。验证String参数不是空且长度小于或等于10@GetMapping("/valid-name/{name}")public void test(@PathVariable(“name”) @NotBlank @Size(max = 10) String username) { // …}任何名称参数超过10个字符的请求都会导致以下错误消息:There was an unexpected error (type=Internal Server Error, status=500).createUser.name:size must be between 0 and 10通过在@Size注解中设置message参数,可以覆盖默认消息。其实我们可以看到校验@RequestParam和@PathVariable参数和我们校验@RequestBody方式一致,只不过一个是写在了实体中,一个写在了外部,当然我们也可以将@RequestParam的参数写入到实体类中,进行使用@RequestParam注解进行引入,比如我们使用一个分页的实例分页实体类/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * “License”); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. /package com.zhuanqb.param.page;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;import org.hibernate.validator.constraints.NotBlank;import org.hibernate.validator.constraints.NotEmpty;import javax.validation.constraints.Max;import javax.validation.constraints.Min;import javax.validation.constraints.NotNull;/* * PageParam <br/> * 描述 : PageParam <br/> * 作者 : qianmoQ <br/> * 版本 : 1.0 <br/> * 创建时间 : 2018-09-23 下午7:40 <br/> * 联系作者 : <a href=“mailTo:shichengoooo@163.com”>qianmoQ</a> */@Data@ToString@NoArgsConstructor@AllArgsConstructorpublic class PageParam { @NotNull(message = “每页数据显示数量不能为空”) @Min(value = 5) @Max(value = 100) private Integer size; // 每页数量 @NotNull(message = “当前页显示数量不能为空”) @Min(value = 1) @Max(value = Integer.MAX_VALUE) private Integer page; // 当前页数 private Boolean flag = true;}@RequestParam调用方式 @GetMapping(value = “list”) public CommonResponseModel findAll(@Validated PageParam param) { … }这样的话可以使我们的校验定制化更加简单。 ...

February 25, 2019 · 2 min · jiezi