SpringBoot:快速使用Spirng

  • 约定大于配置:很多配置springboot已经预设好了(和程序员达成约定);
  • 特点:自动配置起步依赖:依赖传递

spring解决企业级应用开发复杂性而创建的 --> 简化开发

微服务论文原文: 微服务 (martinfowler.com)

微服务论文原文翻译: 微服务(Microservices)——Martin Flower


原理

起步依赖

  1. 项目初始父级依赖spring-boot-dependencies,里面预定义了常用的依赖信息.

     <dependency><groupId>org.apache.activemq</groupId><artifactId>activemq-amqp</artifactId><version>${activemq.version}</version></dependency>
    
  2. 官方依赖初始依赖其他依赖,如spring-boot-starter依赖了spring-boot等

Hello SpringBoot

官网创建SpringBoot项目 Spring Initializr

导入依赖

这里面的 就是版本号从父级继承,springboot会管理所有依赖的版本号

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

包必须和系统的Application类同级

package com.changge.li.springboot.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class Test {@RequestMapping("/hello")public String test(){return "Hello SpringBoot!";}}

application.properties配置文件

# 改变端口号
server.port=8081

启动时艺术字: Spring Boot banner在线生成工具

在resource包下创建banner.txt,会自动被springboot接管

                                                                                                 666                                   6666                         666        66666                            666666666666                       66666       6666                           666666666666                        6666666666   6666                                66666                          6666666666   666666666666                      6666666666666                    6666666666   666666666666                    666666    66666                    6666666666   666666666666          666        666666  66666                      6666666666   666666666666        6666666666     666666666666                       666666666   666666666666           66666666       66666666 666666                   666666     666666666666                          6666666 666666666666                 66666     6666666666666666                      666    666666666666666                 6666            66666666666            66666    66    6666666   66666666                66             6666   66666666666666  6666666   666  666666666   66666666                6           666666666666666666666666        6    66666666  666666 66666666                66666666666666666666666        66    666666 666666666666666666                6666666666666666               66    666666666666666666666666   666666        666666     6666                66     6666    66666666666666    66 666        6666               666       66   66666666666666     666666        6666        66666666666            666666666                       6666       6666666666666666666666                                  66666       66       66666666666666666666666666                    66666                        666666666666666666666666666666        66666                             66666666666666666666666          66666                                 666666666666666              66666                                     66666666                 66666                                        666                   66666                                                               66666                                                               66666                                                               66666                          --by Bootschool.net                  66666                                                                6666                                                                6                                                                  

自动装配原理

详解: https://dwz.cn/P1N121RT

先记住核心就是:springboot会从autoconfigure包下读取spring.factories文件,配置很多自动配置类

@SpringBootApplication//标注是一个springboot应用
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}}

我们找到spring.factories文件,这是自动配置的核心

我们可以看到这里面配置了很多自动配置类

# Auto Configure
org.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.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\

比如我们随便进入一个配置类DispatcherServletAutoConfiguration

可以看到,这些类本身就是交由spirng托管的配置类,并且向其中注入一些bean

这是最终的结果

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)//变成一个配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {/*** The bean name for a DispatcherServlet that will be mapped to the root URL "/".*/public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";/*** The bean name for a ServletRegistrationBean for the DispatcherServlet "/".*/public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";@Configuration(proxyBeanMethods = false)@Conditional(DefaultDispatcherServletCondition.class)@ConditionalOnClass(ServletRegistration.class)@EnableConfigurationProperties(WebMvcProperties.class)protected static class DispatcherServletConfiguration {//注入一个bean@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {

现在我们开始探究本源:回到最初的sprinboot启动类,点进@SpringBootApplication中去

//标明这是一个spirngboot配置类
@SpringBootConfiguration
//告诉springboot:开启自动配置
@EnableAutoConfiguration//扫描包,并且自定义过滤为classes对应的实现了TypeExcludeFilter的类
//XML配置中的元素。
//作用:自动扫描并加载符合条件的组件或者bean
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

进入@EnableAutoConfiguration

//自动配置包
@AutoConfigurationPackage//@import 是Spring底层注解,给容器中导入一个组件
//AutoConfigurationImportSelector :自动配置导入选择器
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

再进入@AutoConfigurationPackage

//自动包注册
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

AutoConfigurationPackages这个类下有一个静态类Registrar

作用:通过反射:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));}@Overridepublic Set<Object> determineImports(AnnotationMetadata metadata) {return Collections.singleton(new PackageImports(metadata));}}

我们再回到@EnableAutoConfiguration,进入AutoConfigurationImportSelector(自动配置导入选择器)

我们看一下他会导入哪些选择器

//我们找到这样一个方法:获取候选的配置类
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());//这个断言告诉我们:springboot会去META-INFspring.factories下找自动配置类,这就是我们最终看到的那个spring.factoriesAssert.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;
}

开始分析getCandidateConfigurations这个方法

//通过bean类加载器,扫描@EnableAutoConfiguration下所有的bean
//返回获取到的所有的配置类集合List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());/**getSpringFactoriesLoaderFactoryClass()返回的就是我们最初在@SpringBootConfiguration上看到的@EnanleAutoConfiguration注解*/
protected Class<?> getSpringFactoriesLoaderFactoryClass() {return EnableAutoConfiguration.class;
}//getBeanClassLoader返回一个bean的类加载器
protected ClassLoader getBeanClassLoader() {return this.beanClassLoader;}

进入loadFactoryNames()

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {//如果类加载器为空,就配置Spring自己的类加载器ClassLoader classLoaderToUse = classLoader;if (classLoaderToUse == null) {classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();}//获取工厂的名字String factoryTypeName = factoryType.getName();//调用下面的loadSpringFactories(),传参类加载器return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {Map<String, List<String>> result = cache.get(classLoader);if (result != null) {return result;}result = new HashMap<>();try {从类加载器中获取资源,就是去读取spring.factories文件:public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";//把获取到的资源封装到result集合中,最后把这个集合返回回去Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);while (urls.hasMoreElements()) {URL url = urls.nextElement();UrlResource resource = new UrlResource(url);Properties properties = PropertiesLoaderUtils.loadProperties(resource);for (Map.Entry<?, ?> entry : properties.entrySet()) {String factoryTypeName = ((String) entry.getKey()).trim();String[] factoryImplementationNames =StringUtils.commaDelimitedListToStringArray((String) entry.getValue());for (String factoryImplementationName : factoryImplementationNames) {result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>()).add(factoryImplementationName.trim());}}}// Replace all lists with unmodifiable lists containing unique elementsresult.replaceAll((factoryType, implementations) -> implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));cache.put(classLoader, result);}catch (IOException ex) {throw new IllegalArgumentException("Unable to load factories from location [" +FACTORIES_RESOURCE_LOCATION + "]", ex);}return result;
}

自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure.包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中

最终结论

  1. SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
  2. 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;

run 方法做的的四件事

1、推断应用的类型是普通的项目还是Web项目

2、查找并加载所有可用初始化器设置到initializers属性

3、找出所有的应用程序监听器设置到listeners属性

4、推断并设置main方法的定义类找到运行的主类

yaml(以数据为核心)语法

student:name: 李长歌age: 18sex: 女students: {name: 李世民,age: 20,sex: 男}person:- student- teacher- manpersons: [student,man,woman]

为属性赋值

@Value和Environment

@Value("${person.name}")String name;@AutowiredEnvironment environment;@RequestMapping("/test")public void test(){System.out.println(name);System.out.println(environment.getProperty("person.age"));;}

如果报springboot未配置注解配置器,加依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><!--这里相当于final的意思--><optional>true</optional>
</dependency>
//指定读取 yaml文件中的对象
@ConfigurationProperties(prefix = "student")
//变成一个组件可以被springboot扫描到
@Component
@Data
public class Student {private String name;private int a_age;private char sex;}
person:name: 李世民student:# 先取值person.name,再拼接李长歌# 松散绑定就是Name可以对应name,a_Age对应a_ageName: ${person.name}李长歌a_Age: 18sex: 女

JSR303校验

<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId>
</dependency>
import org.springframework.validation.annotation.Validated;import javax.validation.constraints.Email;@ConfigurationProperties(prefix = "student")
@Component
@Data
@Validated
public class Student {@Email(message = "请输入邮箱地址")private String name;

多环境配置

  • 根本目的是为了多环境配置之间的互补

在一个yml文件中进行多环境配置

# 这一个段落中的所有配置都会生效
---
server:port: 8081
spring:profiles: dev
------
server:port: 8082
spring:profiles: pro
------
server:port: 8083
spring:profiles: test
---spring:profiles:active: pro

文件路径配置优先级
永远是config大于根路径

@Deprecated
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {//官方源码告诉我们优先级从低到高// Note the order is from least to most specific (last one wins)private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";

student:name: 1
server:port: 8081# 启用哪个环境
spring:profiles:active: dev# 多环境配置
---
student:name: 2
server:port: 8082spring:profiles: dev---
student:name: 3
server:port: 8083spring:profiles: test

用虚拟机或者命令行参数
-Dserver.port=8083

外部环境配置:在这个目录下放配置文件,优先级也是config>根
F:\JAVA\SpringBoot_itcast\target> java -jar .\SpringBoot-0.0.1-SNAPSHOT.jar --server.port
=8086


自动配置原理与yaml配置之间的关系

springboot中的依赖,就是一个个start(启动器),如springboot-start-web

各种自动配置类,在springboot启动时自动加载,其间的各种属性都在yaml文件有对应:修改yaml文件中的属性,就相当于修改了这些自动配置类中的属性

在yaml中直接点进属性,可以进入到类中,看到源码

//我们点进server.port,发现:每个配置都对应着一个Properties类
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
# 控制台会显示有哪些配置被加载了
debug: true

SpringBootWeb

在创建项目的时候,可以直接勾选springbootweb的支持

静态资源

默认放在webjars目录下(一个存放各种web相关依赖的网站) WebJars - Jars 中的 Web Libraries

这个包在他们官网导入maven依赖后,会出现一个对应的xxxwebjars包,里面有项目结构

原理

进入WebMvcAutoConfiguration

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {//如果已经添加了资源映射了,就用用户自己配置的if (!this.resourceProperties.isAddMappings()) {logger.debug("Default resource handling disabled");return;}//添加一个资源映射:访问域名/webjars/**时,等于访问/META-INF/resources/webjars/addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");//同样的再添加路径, 这次是访问域名是mvcProperties,对应的路径是resourcePropertiesaddResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {//默认加入官方配置的路径registration.addResourceLocations(this.resourceProperties.getStaticLocations());//如果已经配置了上下文if (this.servletContext != null) {//那就把用户自己配置的上下文加到静态资源处理中去ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);registration.addResourceLocations(resource);}});
}
//mvcProperties路径默认为:项目下所有资源
public String getStaticPathPattern() {return this.staticPathPattern;
}private String staticPathPattern = "/**";

官方默认配置的资源路径

public String[] getStaticLocations() {return this.staticLocations;
}private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/" };

如果用户自己配置路径的话,就是项目下所有资源都能访问

spring:mvc:static-path-pattern: /hello
private static final String SERVLET_LOCATION = "/";

yaml中已经指定了静态资源存放目录,那么官方配置就会失效

spring:mvc:static-path-pattern: /helloweb:resources:static-locations: classpath:static/index.html

首页和标题图标

欢迎页面默认是官方默认静态资源下面的index.html,也就是静态资源目录放的地方都行

还是WebMvcAutoConfiguration,先跟着注释看一遍就行了

@Bean
//1.欢迎页面处理器映射
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {//9.只要对象创建成功,这里面就已经将index.html页面作为默认的欢迎页了WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, //2.这里获取一个欢迎页面getWelcomePage(),this.mvcProperties.getStaticPathPattern());welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());return welcomePageHandlerMapping;
}//3.获取欢迎页面
private Resource getWelcomePage() {//4.读取官方的静态资源默认存放地址(上面静态资源那里有讲解)for (String location : this.resourceProperties.getStaticLocations()) {//通过一个路径地址,获取一个index.html页面Resource indexHtml = getIndexHtml(location);//8.接收得到的index.html资源,返回这个资源作为首页页面if (indexHtml != null) {return indexHtml;}}//这里是如果我们自己配置了首页路径,就用我们自己的ServletContext servletContext = getServletContext();if (servletContext != null) {return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION));}return null;
}//5.获取index.html页面
private Resource getIndexHtml(String location) {//6. 这里调用了下面的一个重载方法,传参还是上面的默认静态资源路径return getIndexHtml(this.resourceLoader.getResource(location));
}private Resource getIndexHtml(Resource location) {try {//7.这里在默认的静态资源路径下创建了一个相对的index.htmlResource resource = location.createRelative("index.html");//把这个index.html作为资源返回回去if (resource.exists() && (resource.getURL() != null)) {return resource;}}catch (Exception ex) {}return null;
}

图标在2.6.7版本已经弃用了

2.1.7版本测试图标

spring:mvc:favicon:# 关闭默认图标enabled: false

图标和静态资源放在一起

WebMvcAutoConfiguration

@Configuration
//value告诉我们yaml中应该怎么配置属性
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration implements ResourceLoaderAware {private final ResourceProperties resourceProperties;private ResourceLoader resourceLoader;public FaviconConfiguration(ResourceProperties resourceProperties) {this.resourceProperties = resourceProperties;}@Overridepublic void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader = resourceLoader;}@Beanpublic SimpleUrlHandlerMapping faviconHandlerMapping() {SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);//静态资源路径下的只要是favicon.ico就会被认为是标题图标mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", //3.网站图标请求处理程序faviconRequestHandler()));return mapping;}//4.网站图标请求处理程序@Beanpublic ResourceHttpRequestHandler faviconRequestHandler() {ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();//5.解决网站图标位置requestHandler.setLocations(resolveFaviconLocations());return requestHandler;}//6.解决网站图标位置private List<Resource> resolveFaviconLocations() {String[] staticLocations = getResourceLocations(this.resourceProperties.getStaticLocations());List<Resource> locations = new ArrayList<>(staticLocations.length + 1);//调用的还是官方默认的静态资源路径Arrays.stream(staticLocations).map(this.resourceLoader::getResource).forEach(locations::add);locations.add(new ClassPathResource("/"));return Collections.unmodifiableList(locations);}

MVC自定义配置

spirngboot官方文档告诉我们

1.1.1. Spring MVC Auto-configuration//如果你想要保持自己的spirngBoot定制,或者使用更多的定制的话,你可以添加你自己的有@Configuration的WebMvcConfigurer类,但是不能有@EbanbleWebMvc
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.Locale;//我们自己的配置类,相当于dispatchServlet
@Configuration
public class MyConfig implements WebMvcConfigurer {//把自定义视图解析器加到容器中去@Beanpublic ViewResolver getViewResolver(){return new MyViewResolver();}//自定义视图解析器,可以不做,用官方默认的不影响使用class MyViewResolver implements ViewResolver{@Overridepublic View resolveViewName(String viewName, Locale locale) throws Exception {return null;}}//自定义自己的视图跳转链接@Overridepublic void addViewControllers(ViewControllerRegistry registry) {registry.addViewController("/pwdModify.html").setViewName("pwdModify");registry.addViewController("/fileUpload.html").setViewName("fileUpload");registry.addViewController("/register.html").setViewName("register");registry.addViewController("/useradd.html").setViewName("useradd");}}

我们去看一眼视图解析器的源码

ViewResolver是个接口,进入实现类ContentNegotiatingViewResolver,他实现了resolveViewName()

@Override
@Nullable
//解析视图名称
public View resolveViewName(String viewName, Locale locale) throws Exception {RequestAttributes attrs = RequestContextHolder.getRequestAttributes();Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());if (requestedMediaTypes != null) {//根据传入的视图名称,获取所有候选的视图List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);//在所有候选视图中获取一个最好的视图返回View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);if (bestView != null) {return bestView;}}}@Nullable
//获取最好的视图
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {for (View candidateView : candidateViews) {if (candidateView instanceof SmartView) {SmartView smartView = (SmartView) candidateView;/**这里返回的永远是true:public boolean isRedirectView() {return true;}*/if (smartView.isRedirectView()) {return candidateView;}}}

这里在源码DispatcherServlet类的doDispatcher()打断点,然后调试,访问主页后,可以在debug中找到一个viewResolver对象,里面可以看到我们自己配置的视图解析器.


默认的格式配置类在WebMvcProperties类中

public static class Format {/*** Date format to use, for example 'dd/MM/yyyy'.*/private String date;

@EnableWebMvc源码

本身就只是导入了一个类DelegatingWebMvcConfiguration

@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {}
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();@Autowired(required = false)//获取所有WebMvcConfigurer并放到spirngmvc容器中去public void setConfigurers(List<WebMvcConfigurer> configurers) {if (!CollectionUtils.isEmpty(configurers)) {this.configurers.addWebMvcConfigurers(configurers);}}

但是我们去看一下自动配置类的源码

//当容器中没有WebMvcConfigurationSupport时,自动配置才会生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
public class WebMvcAutoConfiguration {

而我们刚才的DelegatingWebMvcConfiguration类,却继承了WebMvcConfigurationSupport

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

所以:当我们要自定义mvc配置时,如果加了@EnableWebMvc后,自动配置崩盘


Thymelafe模板引擎 百里香叶 (thymeleaf.org)

动静分离:静态资源(不会改变数据)和动态模板(页面中的数据是会变化的)分开开发,可以理解成分开两个目录存放

模板引擎Thymeleaf快速入门_哔哩哔哩_bilibili

Thymeleaf快速入门 | lookroot的个人空间

引入时的版本号要比springboot版本多一个整数,如springboot是2.0,那thymelafe就要3.0

<!--引入thymeleaf的依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

只有加入thymelafe支持后,才能访问templates目录下面的资源

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;//默认的前缀public static final String DEFAULT_PREFIX = "classpath:/templates/";//默认的后缀public static final String DEFAULT_SUFFIX = ".html";
@Controller
public class Test {@RequestMapping("/hello")public String test(){//会默认变成/templates/index.htmlreturn "index";}}

基本语法

html标签的所有属性都能被thymelafe接管,需要引入头文件

<html lang="en" xmlns:th="http://www.thymeleaf.org"><!--${文本取值}--><h1 th:text="${msg}"></h1>

常用使用

<!--引入组件:common目录下的head.html文件中的 名叫comon_head的组件-->
<th:block th:include="~{common/head.html :: common_head}"></th:block><div class="right"><div class="location"><strong>你现在所在的位置是:</strong><span>客户管理页面</span></div><div class="search"><!--@{动态请求:这里对应的是controller中的/jsp/user.do请求--><form method="get" th:action="@{/jsp/user.do}"><input name="method" value="query" class="input-text" type="hidden"><span>客户名:</span><input name="queryName" class="input-text" type="text" th:value="${queryUserName}"><span>客户角色:</span><select name="queryUserRole"><option value="0">--请选择--</option><!--循环遍历后台传过来的roleList集合,变成一个个的role对象th:object = 定义一个role对象,这里会自动取值th:each中role对象*{id} = ${role.id}th:selected="*{id} == ${queryUserRole}" 当id = queryUserRole时,这个potion加上selected属性--> <option  th:each="role:${roleList}" th:object="${role}"th:value="*{id}" th:text="*{roleName}"th:selected="*{id} == ${queryUserRole}"></option></select><input type="hidden" name="pageIndex" value="1"/><input value="查 询" type="submit" id="searchbutton"><!--动态请求项目目录/下的useradd.html页面--><a th:href="@{/useradd.html}">添加客户</a></form></div><table class="providerTable" cellpadding="0" cellspacing="0"><tr class="firstTr"><th width="10%">客户编码</th><th width="20%">客户名称</th><th width="10%">性别</th><th width="10%">年龄</th><th width="10%">电话</th><th width="10%">客户角色</th><th width="30%">操作</th></tr><tr th:each="user:${userList}" th:object="${user}"><td><!--等同于写在span中的属性th:text="*{userCode}"-->  <span>[[*{userCode}]]</span></td><td><span>[[*{userName}]]</span></td><td><span><!--当gender==1时,男才会显示,否则不显示--><span th:if="*{gender==1}">男</span><span th:if="*{gender==2}">女</span></span></td><td><span>[[*{age}]]</span></td><td><span>[[*{phone}]]</span></td><td><span>[[*{userRoleName}]]</span></td><td><span><a class="viewUser" href="javascript:;" th:userid="*{id}" th:username="*{userName}"><img th:src="@{/images/read.png}" alt="查看" title="查看"/></a></span><span><a class="modifyUser" href="javascript:;"  th:userid="*{id}" th:username="*{userName}"><img th:src="@{/images/xiugai.png}" alt="修改" title="修改"/></a></span><span><a class="deleteUser" href="javascript:;"  th:userid="*{id}" th:username="*{userName}"><img th:src="@{/images/schu.png}" alt="删除" title="删除"/></a></span></td></tr></table><input type="hidden" id="totalPageCount" th:value="${totalPageCount}"/><!--插入组件并且传参totalCount=${tatalCount} 这里的${totalCount}会自动从页面中取值--><div th:insert="~{rollpage :: rollPage(totalCount=${totalCount},currentPageNo=${currentPageNo},totalPageCount=${totalPageCount})}"></div></div>
</section><!--点击删除按钮后弹出的页面-->
<div class="deleteUserButton"></div>
<div class="remove" id="removeUse"><div class="removerChild"><h2>提示</h2><div class="removeMain"><p>你确定要删除该客户吗?</p><!--href="#" 点击后不刷新页面,但是会回到页面顶部--><a href="#" id="yes">确定</a><!--执行完函数后,不刷新页面--><a onclick="clearBtn();return false;">取消</a></div></div>
</div><th:block th:include="~{common/foot.html :: common_foot}"></th:block>
<script type="text/javascript" th:src="@{/js/userlist.js}"></script>

项目实战

alt+鼠标:多行特定区域操作

主页

静态资源(数据不会变的资源),可以放在static目录(服务器默认去资源的目录)下

写地址时,static目录不用写:如8080/static/index.html,直接写成8080/index.html

访问resources 下的 static 下的 images中pppp.png的资源写法

background: url("/images/pppp.png");
spring:thymeleaf:# 不关闭,thymeleaf会保留上一次的模板缓存cache: falseserver:servlet:# 上下文路径context-path: /

页面放在templates目录下,自动被thymeleaf接管作为动态模板

thymeleaf源码

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;//默认的前缀,在每个controller跳转页面时自动加上public static final String DEFAULT_PREFIX = "classpath:/templates/";//默认的后缀,同前缀一样public static final String DEFAULT_SUFFIX = ".html";

默认是转发

@RequestMapping("/toMain")public String toMain1(){//转发到classpath:/templatess/index.htmlreturn "index";}

重定向并携带参数

return "redirect:/home/"+userService.selectPasswordByUserName(userName).getUid()+"/1";//后台restful接收
@GetMapping("/home/{uid}/{pageNum}")

国际化

确认已经安装Resources Bundle Editor插件

在resources目录下创建国际化所需的文件(创建i18n目录后,直接创建文件,idea会自动帮我们创建资源包文件夹),

默认是login.properties,英文加后缀_en_US,等

点击’资源’启用Resources Bundle插件的可视化配置

applecation中指定国际化文件所有目录地址

spring:messages:# 配置文件的真实路径:i18n下的资源包loginbasename: i18n.login

thymeleaf模板中用#{}来接收国际化消息

<!--接收名为username的国际化消息-->
<input th:placeholder="#{username}">
<input th:placeholder="#{password}">
<input th:value="#{submit}">

springboot监控国际化

首先向后台发送切换语言的请求

<a th:href="@{/index.html(language='zh_CN')}">中文</a>
<a th:href="@{/index.html(language='en_US')}">英文</a>

实现LocaleResolver配置国际化

package com.changGe.li.configurers;import com.mysql.cj.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;public class I18n implements LocaleResolver {@Overridepublic Locale resolveLocale(HttpServletRequest request) {//获取计算机默认的国家和地区Locale locale = Locale.getDefault();String language = request.getParameter("language");if(!StringUtils.isNullOrEmpty(language)){String[] split = language.split("_");//国家,地区locale = new Locale(split[0],split[1]);}return locale;}//存入地区,可以不实现@Overridepublic void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {}}

在我们自己的WebMvcConfigurer中声明国际化组件

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {@Beanpublic LocaleResolver localeResolver(){return new I18n();}

成品效果


登录拦截器

拦截器依旧是实现HanderItercepter

import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static com.changGe.li.util.UserConstant.USER_SESSION;public class ForgoLogin implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {Object user = request.getSession().getAttribute(USER_SESSION);if(user == null){request.setAttribute("error","请先登录,谢谢!");request.getRequestDispatcher("/").forward(request,response);return false;}return true;}}

配置拦截和过滤请求

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new ForgoLogin())//拦截所有请求.addPathPatterns("/**")//过滤请求,因为是作用于登录,把所有和登录有关的都要过滤掉,不然就不造成死循环.excludePathPatterns("/toMain","/login","/index.html","/","/css/**","/js/**","/scss/**","/calendar/**","/fonts/**","/images/**");}}

错误和注销

把404.html,500t.html等模板,放在error目录下,出现404错误时,springboot就自动匹配他们

调用qq的公益404页面: http://www.qq.com/404/

<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="description" content="公益404页面是由腾讯公司员工志愿者自主发起的互联网公益活动。"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1"><!--title图标--><link rel="icon"href="data:image/gif;base64,R0lGODdhIAAgANUAAAAAAAgFBgYICAwMDBAPDxAQDxQTFBUYFxcaGRwcHCQkJCQoJykqKTQ0ND09PUJCQktMTFZWVltcXF1hYGNjY2doaGpqanNzc3d5eHp6eoODg4uLi5eXl5mamqOjo62tra+wr7S0tLe5uLu7u7/AwMPEw83NzdbX1tfa2dra2uTk5Ovr6+/w7/T09Pj39/f4+P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAADEALAAAAAAgACAAAAb/QJhwOGyZOpIGQ6FgNCQcU4tIrcJaIwlgy+1uJaOplZqydBOSzGaTkSS6lNN4ONoOAJRQajV0rVAgFF0jcyEAbw9hc1cjD4cAIVZ1DAAXKotEKxkAlIREJ48bYphFGwAKAHJCLWaVo6RFF1sWYnUDDpewVSoOd4QtWgCeMCmXLaqfUyopdF9GWxRiKQAVKh3CRHUdKhQEzFeCAEdbH6uOh28A3ypbCQhbEGIfWx2CCd8wJhB2CpFDIQvuAIBgYkiKN0kASOAzRJYjBwyFrGgAwNGFTFqUAMjwYogpA1woMFwRDgDIDUQ2LQGAcoiGLRr2QRgp8yUADURMMWFJRYMH3BglJOQSwq0EDA84c56ilOGVLisqKUaISKVFihIjRJRI4XTIiggAGggygKxICAoguSDI0/UEyAnXAJTLJMtLlwxUYcwD0MEEtIgtBKGi0OHDh3qn8IwKvEVKuGHXGAwI8apFCAOUOvxzBqNOAVzgthi1UgKasl7YrtS9MMXI6DEjpKieNWoaSA1dF7V4+QafkNKoLOlSIQsVCUkmATgYwWIRCxEOAByANKc0AAEAJnzYI6bFihQfBAUQjSlF3S0GIqjZgCFC2i0XfM/BUtJuSEVPjdRTwsQJYdljBAEAOw=="><title>404 您访问的页面搞丢了</title><script src="https://volunteer.cdn-go.cn/404/latest/404.js" homePageUrl="http://localhost:8080/" homePageName="返回首页"></script></head>

一个国外的UI网站: Semantic UI (semantic-ui.com)


文件上传与下载

spring:servlet:multipart:# 服务器和客户端文件上限max-file-size: 100MBmax-request-size: 100MB
<div class="box"><form action="/upload" method="post" enctype="multipart/form-data"><a href="javascript:;" id="dj"><span class="jia">+</span>文件上传</a><h2 id="wjxg">选中一个文件开始上传<span>[[${msg}]]</span></h2><input type="file" name="filed" value="" id="wj" style="display: none"><input type="submit" value="提交文件" id="tij"></form><a th:href="@{/download}">文件下载</a>
</div>
<script>let a = document.querySelector("#dj");let fli = document.querySelector("#wj");function getFli() {fli.click();}a.addEventListener("click", function () {getFli();})
</script>
//文件上传
@PostMapping("/upload")
public String fileUpload(@RequestParam("filed") MultipartFile multipartFile,Model model) throws Exception{// 上传的位置String path = ResourceUtils.getURL("classpath:").getPath()+"static/upload/";// 判断路径是否存在File file = new File(path);if(!file.exists()){file.mkdirs();}// 获取上传文件的名称String filename = multipartFile.getOriginalFilename();// 把文件的名称设置唯一值,uuidString uuid = UUID.randomUUID().toString().replace("-", "");filename = uuid+"_"+filename;// 完成文件上传multipartFile.transferTo(new File(path,filename));//上传到数据库中:注意InputStream要访问的应该是一个文件,而不是文件夹,不然就FileNotFundException,说拒绝访问userMapper.insertImage(new FileInputStream(path+filename), path, filename);model.addAttribute("msg","文件上传成功");return "fileUpload";
}//文件下载
@RequestMapping( "/download")
public String downloads(HttpServletResponse response ) throws Exception{//要下载的图片地址String path = ResourceUtils.getURL("classpath:").getPath()+"static/images/";String fileName = "buy.png";//设置页面不缓存,清空response.reset();//字符编码response.setCharacterEncoding("UTF-8");//设置响应头response.setHeader("Content-Disposition", "attachment;fileName="+ URLEncoder.encode(fileName, "UTF-8"));//设置响应头:以二进制传输数据response.setContentType("multipart/form-data");File file = new File(path,fileName);//2、 读取文件--输入流InputStream input=new FileInputStream(file);//3、 写出文件--输出流OutputStream out = response.getOutputStream();byte[] buff =new byte[1024];int index=0;//4、执行 写出操作while((index= input.read(buff))!= -1){out.write(buff, 0, index);out.flush();}out.close();input.close();return null;}

UserMapper

//添加图片
int insertImage(@Param("inputStream")InputStream inputStream,@Param("realPath")String realPath,@Param("fileName")String fileName);

UserMapper.xml

<insert id="insertImage">insert into imageTest(image,filePath,fileName) values(#{inputStream},#{realPath},#{fileName})
</insert>

普通邮件发送

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId>
</dependency>
spring:mail:host: smtp.qq.com #发送邮件服务器password: gxxiopidepqpdbja #客户端授权码#端口号465或587port: 465properties:mail:smtp:auth: true #授权socketFactory: ##SSL协议工厂获得SSL协议class: javax.net.ssl.SSLSocketFactorydebug: trueprotocol: smtp #协议
<script type="text/javascript">$(function (){})function sendEmail(){let email = $("#email").val();let textarea = $("#textarea").val();$.ajax({type:"post",url:"/jsp/register",data:{email:email,textarea:textarea},dataType:"json",success:function (){//刷新当前页面,并且不走缓存,从服务器重新获取数据window.location.reload(true);},error:function(){alert("邮件未发送成功");}});}</script>
</head>
<body><div class="wbk_box"><form method="post" onsubmit="sendEmail()"><input type="text" name="email" id="email" value="@qq.com" placeholder="输入您的qq邮箱地址,将接收到一封邮件" class="yht"><textarea name="textarea" id="textarea" class="shurk">什么是thymeleaf...</textarea><input type="submit" value="发送" class="fas"><span style="font-size: 13px">验证码为:<span id="msg"></span> [[${msg}]]</span></form></div>
</body>
@Resource
//邮件发送对象
private JavaMailSenderImpl mailSender;@RequestMapping("/jsp/register")
@ResponseBody
public void register(@RequestParam(value = "email") @Email String takeOver,@RequestParam(value = "textarea",required = false) String context,Model model) throws Exception{//多线程发送验证码Callable callable = new Inner(takeOver,context);String verifyCode = (String)callable.call();model.addAttribute("msg",verifyCode);//return  verifyCode;}//多线程发送邮件的内部类
class Inner implements Callable{private volatile String takeOver;private volatile String context;public Inner(String takeOver,String context){this.takeOver = takeOver;this.context = context;}@Overridepublic String call(){//一个唯一的验证码String code = UUID.randomUUID().toString();try {return code;}finally {SimpleMailMessage simpleMailMessage = new SimpleMailMessage();simpleMailMessage.setTo(takeOver);simpleMailMessage.setFrom("@qq.com");//邮件主题和内容simpleMailMessage.setSubject(".com注册验证码");simpleMailMessage.setText("您好验证码是:"+ code + context);//这个类可以用来设置一些基本属性,如发送方,端口号等mailSender.setUsername("@qq.com");//发送邮件mailSender.send(simpleMailMessage);}}//call()
}//class

在线人数

监听器

package com.changGe.li.listeners;import org.springframework.stereotype.Component;import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;@Component
public class OnlineListener implements HttpSessionListener {//有session被创建时@Overridepublic void sessionCreated(HttpSessionEvent se) {//在最高的作用域servletContext操作sessionServletContext servletContext = se.getSession().getServletContext();Integer online = (Integer) servletContext.getAttribute("online");if(online == null || online <= 0){online = 1;}else {online ++;}servletContext.setAttribute("online",online);}//有session被销毁时@Overridepublic void sessionDestroyed(HttpSessionEvent se) {ServletContext servletContext = se.getSession().getServletContext();Integer online = (Integer)servletContext.getAttribute("online");if(online == null || online < 0){online = 0;}else {online --;}servletContext.setAttribute("online",online);}}
@RequestMapping("/online")
public String online(HttpServletRequest req,HttpServletResponse resp, Model model) throws ServletException, IOException {//一秒刷新一次resp.setHeader("refresh","1");//一定要从ServletContext中取值,ServletContext监控所有sessionString online = String.valueOf(req.getServletContext().getAttribute("online"));model.addAttribute("online",online);return "online";
}

分页

@RequestMapping(params = "method=query")
private String userList(@RequestParam(value = "queryName",required = false) String username,//设置defaultValue时,自动required = false@RequestParam(value = "queryUserRole",defaultValue = "0") @NumberFormat int queryUserRole,//NumberFormat 格式化成数字类型,但是注意:如果没有数据传过来时,会把null传给queryUserRole@RequestParam(value = "pageIndex",defaultValue = "1") @NumberFormat int parseInt,Model model){//用户总数int totalCount = userService.queryUserCount(username,queryUserRole);//总页数 = (总记录数 + 每页个数 - 1) / 页个数int totalPageCount = (totalCount + pageSize - 1)/ pageSize;//限定当前页不超过0~总页数if(parseInt < 1){parseInt = 1;}if(parseInt > totalPageCount){parseInt = totalPageCount;}//用户列表,角色列表和用户总数List<User> users = userService.queryUserList(username,queryUserRole,parseInt,pageSize);List<Role> roles = userService.queryRoleList();//将前端页面需要的数据放入请求中model.addAttribute("userList",users);model.addAttribute("roleList",roles);model.addAttribute("queryUserName",username);model.addAttribute("queryUserRole",queryUserRole);model.addAttribute("totalCount",totalCount);model.addAttribute("totalPageCount",totalPageCount);model.addAttribute("currentPageNo",parseInt);return "userList";
}

网站开发


SpringBootJDBC

可以在创建项目时勾选jdbc和Mysql driver

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
spring:datasource:password: rootusername: rooturl: jdbc:mysql:///smbms?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Driver

springboot默认的数据源,注意test时的类一定要和java包下的包结构相同

package java.com.changGe.li;import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import javax.sql.DataSource;
import java.sql.Connection;@SpringBootTest
@RunWith(SpringRunner.class)
public class Test {@AutowiredDataSource dataSource;@org.junit.jupiter.api.Testvoid test() throws Exception{//查看默认数据源Class<? extends DataSource> aClass = dataSource.getClass();System.out.println(aClass);Connection connection = dataSource.getConnection();}}

数据源模板JdbcTemplate

@Resource
JdbcTemplate jdbcTemplate;List<Role> roles =  jdbcTemplate.query("select * from smbms_role", new BeanPropertyRowMapper<>(Role.class));

Druid天生自带监控的数据源

<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.8</version>
</dependency>

把springboot默认的数据源改成Druid

spring:datasource:type: com.alibaba.druid.pool.DruidDataSource
#SpringBoot默认是不注入这些的,需要自己绑定
#druid数据源专有配置
spring:datasource:password: rootusername: rooturl: jdbc:mysql:///smbms?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Drivertype: com.alibaba.druid.pool.DruidDataSourcedruid:initial-size: 5min-idle: 5max-active: 20max-wait: 60000timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: true#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入#如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity#则导入log4j 依赖就行filters: stat,wall,log4jmaxPoolPreparedStatementPerConnectionSize: 20useGlobalDataSourceStat: trueconnectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

Druid后台监控配置

自定义Druid的配置类,相当于我们自定义一些web.xml(springboot内部集成)的配置

package com.changGe.li.configurers;import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;@Configuration
public class DruidConfig {//替换默认的数据源@ConfigurationProperties(prefix = "spring.datasource")@Beanpublic DataSource dataSource(){return new DruidDataSource();}@Beanpublic ServletRegistrationBean servletRegistrationBean(){//配置访问druid页面的访问路径是/druid/*ServletRegistrationBean<StatViewServlet> statViewServlet = new ServletRegistrationBean<>(new StatViewServlet(),"/druid/*");Map<String, String> map = new HashMap<>();//登录页面的用户map.put("loginUsername","admin");map.put("loginPassword","123456");//""所有人都可以登录map.put("allow","");//不允许谁登录map.put("admin","192.168.1.1");statViewServlet.setInitParameters(map);return statViewServlet;}@Beanpublic FilterRegistrationBean registrationBean(){FilterRegistrationBean<Filter> filter = new FilterRegistrationBean<>();//釆用阿里的web过滤器filter.setFilter(new WebStatFilter());Map<String, String> map = new HashMap<>();//这些请求不过滤,以免死循环map.put("exclusions","*.js,*.css,/druid/*");filter.setInitParameters(map);return filter;}}

配置好后访问这个地址Druid Stat Index ,就可以看到druid后台监控数据库信息


整合MyBatis

yaml设置别名和xml文件扫描

mybatis:# 给com.changGe.li.pojo包下所有类自动设置别名type-aliases-package: com.changGe.li.pojo# 标明mapper.xml文件位置mapper-locations: classpath:com/changGe/li/mappers/*.xml

UserMapper接口和mapper.xml直接可以拿来用,不用变

用**@Mapper**标明这个类是一个Mapper

@Mapper
public interface UserMapper {

也可以在springboot主启动类上,用**@MapperScan(“包路径”)直接将包下所有类注册成Mapper**

@SpringBootApplication
@MapperScan(basePackages = "com.changGe.li.mappers")
public class Application {

SpringSecurity安全框架

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>

认证与授权

@EnableWebSecurity//开启securiry,本质是AOP拦截器
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http//自定义登录页面index.html.formLogin().loginPage("/index.html").usernameParameter("userCode")//自定义表单参数.loginProcessingUrl("/login")//登录时要访问的controller路径,不配置security不会执行UserDetailsService.defaultSuccessUrl("/toFrame")//成功后重定向.and().logout().logoutUrl("/logout")//自定义退出.logoutSuccessUrl("/toFrame")// and表示方法链结束,重新回到http对象那里.and()// 没有权限后跳转的页面.exceptionHandling().accessDeniedPage("/error/403.html")//关闭检查跨站请求伪造.and().csrf().disable()//自定义用户认证.userDetailsService(new UserDetailsServiceImpl())/*** 从上到下匹配,一旦匹配到就不再匹配了*/.authorizeHttpRequests()//所有请求都需要有授权才能访问//权限授权.antMatchers("/","/index.html","/login","/logout","/toFrame","/templates/common/**","/templates/error/**","/calendar/**", "/css/**","/fonts/**","/images/**","/js/**","/scss/**").permitAll().and()//开启记住我,前端参数为rememberMe,cookie存储时间为10000秒.rememberMe().rememberMeParameter("rememberMe").tokenValiditySeconds(10000);}}

连接数据库认证

web加载顺序:context-param -> listener -> filter -> servlet -> spring

security会在spring之前加载,所以有时会造成自动注入失效

package com.changGe.li.util;import com.changGe.li.mappers.UserMapper;
import com.changGe.li.pojo.User;
import com.mysql.cj.util.StringUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;//@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {private static UserMapper userMapper;@Resourcepublic void setUserMapper(UserMapper userMapper){this.userMapper = userMapper;}private static HttpServletRequest httpServletRequest;@Resourcepublic void setHttpServletRequest(HttpServletRequest httpServletRequest){this.httpServletRequest = httpServletRequest;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.getUserByUsername(username);if (user == null) {httpServletRequest.setAttribute("error", "用户名或密码错误");//throw new UsernameNotFoundException("用户不存在");}httpServletRequest.getSession().setAttribute("user", user);//设置用户权限List<SimpleGrantedAuthority> authorities = new ArrayList<>();if (!StringUtils.isNullOrEmpty(user.getUserRoleName())) {authorities.add(new SimpleGrantedAuthority(user.getUserRoleName()));}//用户认证return new org.springframework.security.core.userdetails.User(user.getUserName(), "{bcrypt}"+new BCryptPasswordEncoder().encode(user.getUserPassword()), authorities);}}

注解式授权

@SpringBootApplication
@MapperScan(basePackages = "com.changGe.li.mappers")
//启动security注解,启动pre和postEnable注解
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class Application {
@RequestMapping(params = "method=delUser")
@ResponseBody
@PreAuthorize("hasAuthority('系统管理员')")
private String delUserUser(@RequestParam("uid") @NumberFormat Integer uid){//有这个人if(userService.queryUserById(uid) != null){//删除成功if(userService.dropUserById(uid) > 0){return "true";}else {return "false";}}else {return "notExist";}
}

动态菜单

<dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--命名前缀-->
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" th:fragment="common_head">

登录后才会显示

<a sec:authorize="isAuthenticated()" th:href="@{/logout}">退出</a>

有这个权限才会显示

<li sec:authorize="hasAnyAuthority('系统管理员','经理')">

角色信息会被封装进principal对象中

<!--th:text="${#httpServletRequest.getSession().getAttribute('user').getUserName()}"--><!--用户名和角色-->
<span sec:authentication="name"></span>
<span sec:authentication="principal.authorities"></span>

记住我

开启后会在本地存储一个cookie,下次访问时,先去看本地有没有这个cookie

<input type="checkbox" name="rememberMe">记住我</input>
//开启记住我,前端参数为rememberMe,cookie存储时间为10000秒
.rememberMe().rememberMeParameter("rememberMe").tokenValiditySeconds(10000);

Shiro

有自己的session

快速开始

官方的用户名和密码是随机设置的

<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.9.0</version>
</dependency>

log4j.properties

log4j.rootLogger=INFO, stdoutlog4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p 【%c】 - %m %n# General Apache libraries
log4j.logger.org.apache=WARN# Spring
log4j.logger.org.springframework=WARN# Default Shiro logging
log4j.logger.org.apache.shiro=INFO# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

shiro.ini

# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5[users]
# user 'root' with pasword 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the pasword 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with pasword '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with pasword 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with pasword 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QBrUg0Ir-1659497555119)(…/AppData/Roaming/Typora/typora-user-images/1652786395915.png)]

总共三大对象:Subject(当前用户) ,SecurityMenenge(管理中心),和realm(数据)

整合SpringBoot

<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.9.1</version>
</dependency>

自定义Realm

package com.changGe.li.util;import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;public class UserRealm extends AuthorizingRealm {//授权@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {return null;}//认证@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {return null;}}

自定义配置类

package com.changGe.li.configurers;import com.changGe.li.util.UserRealm;
import org.apache.commons.collections.map.LinkedMap;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class ShiroConfig {//配置过滤器工厂@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();/*** 认证* anon不认证,authc认证,user需要有记住我功能,perms要有权限,role要有角色权限*/LinkedMap map = new LinkedMap();//useradd.html需要系统管理员才能访问map.put("/useradd","perms[系统管理员]");factoryBean.setFilterChainDefinitionMap(map);//设置登录请求factoryBean.setLoginUrl("/login");//未授权跳转页面factoryBean.setUnauthorizedUrl("/error/401");return factoryBean;}//配置manager,这里写的是方法名@Beanpublic DefaultWebSecurityManager securityManager(@Qualifier("getRealm") Realm realm){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(realm);return securityManager;}//配置Realm@Beanpublic Realm getRealm(){return new UserRealm();}}

用户认证

@RequestMapping("/login")
public String login(@RequestParam("userCode") String userCode, @RequestParam("password") String password,Model model){Subject subject = SecurityUtils.getSubject();UsernamePasswordToken token = new UsernamePasswordToken(userCode, password);try {//根据令牌登录subject.login(token);return "redirect:/toFrame";}catch (UnknownAccountException e){model.addAttribute("error", "用户名错误");return "index";}catch (IncorrectCredentialsException e){model.addAttribute("error", "密码错误");return "index";}
}

类和类之间没有联系,底层连接.除非debug才能看明白

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;User user = userService.getUser(token.getUsername(), String.valueOf(token.getPassword()));if(user == null){return null;//null会自动匹配对应的异常,如用户存在}//shiro自己做密码认证return new SimpleAuthenticationInfo("",user.getUserPassword(),"");
}

请求授权

认证时把user对象存入session中,授权时可以拿出来

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {//简单的授权信息SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();//获取当前用户Subject subject = SecurityUtils.getSubject();//拿到user对象,这个user对象是在认证方法最后传参的User user = (User)subject.getPrincipal();//给当前用户授权权限信息info.addStringPermission(user.getUserRoleName());return info;
}

底层流程是先认证再走授权

Subject subject = SecurityUtils.getSubject();
//一定要把用户存入session,不然过滤器会监听不到user
subject.getSession().setAttribute(USER_SESSION,user);//shiro自己做密码认证
//把user对象存入session中
return new SimpleAuthenticationInfo(user,user.getUserPassword(),"");

整合thymlafe

<dependency><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>2.0.0</version>
</dependency>
//整合thymeleaf时需要的shiro方言
@Bean
public ShiroDialect getShiroDialect(){return new ShiroDialect();
}

前端命名空间

xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"

前端取值判断

<!--根据属性获取用户信息,反射技术获取私有属性-->
<span style="color: #fff21b" shiro:principal property="userName"></span>
<span style="color: #fff21b" shiro:principal property="userRoleName"></span><!--已经认证了-->
<a shiro:user="" th:href="@{/logout}">退出</a><!--拥有一定权限-->
<li shiro:hasAnyPermissions="系统管理员"><a th:href="@{/pwdModify.html}">密码修改</a></li>
<li shiro:hasAnyPermissions="系统管理员,经理"><a href="/fileUpload.html">上传文件</a></li>
<li shiro:hasAnyPermissions="系统管理员,经理"><a href="/register.html">邮件发送</a></li>
<li shiro:hasAnyPermissions="系统管理员,经理"><a th:href="@{/online}">在线人数</a></li>

Swagger:前后端交互工具

本质就是一个可以实时更新,用于前后端分离的API文档工具

RestFul风格API文档在线自动生成工具

协调沟通,即时解决

<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version>
</dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version>
</dependency>

整体代码

访问网址:locahost:8080/swagger-ui.html

package com.changGe.li.configurers;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;import java.util.ArrayList;//开启Swagger2
@EnableSwagger2
@Configuration
public class swaggerConfig {//多个组@Beanpublic Docket docket1(){return new Docket(DocumentationType.SWAGGER_2).groupName("望江");}//Environment:yml运行配置信息存储//swagger的bean实例是docket@Beanpublic Docket docket(Environment environment){Profiles of = Profiles.of("dev", "test");//监听是否存在某个信息boolean flag = environment.acceptsProfiles(of);System.out.println(flag);Docket docket = new Docket(DocumentationType.SWAGGER_2);docket.apiInfo(apiInfo()).select()/*** RequestHandlerSelectors 可使用的扫描条件:*    basePackage() --- 只扫描指定路径上的类*    any() --- 扫描所有类*    withClassAnnotation() --- 通过判断类上的注解中有xxx注解扫描类*    withMethodAnnotation() --- 通过判断方法上的注解中有xxx注解扫描方法*/.apis(RequestHandlerSelectors.basePackage("com.changGe.li.controllers"))//过滤:扫描com.changGe.li.controllers包下,并且路径前缀是/test的所有接口.paths(PathSelectors.ant("/test/**")).build()//返回的是一个Docket对象。.groupName("长歌").enable(flag);return docket;}@Beanpublic ApiInfo apiInfo(){Contact contact = new Contact("诗长歌","http://www.baidu.com","3276295265@qq.com");return  new ApiInfo("诗长歌的Swagger文档","描述","v1.0","http://www.baidu.com",contact,"执照 1.0","http://执照网址.com",new ArrayList());}}

根据环境配置决定是否开启swagger

上线时环境配置

# 上线时的生产环境
profiles:
active: dev# swagger和springboot高版本路径冲突,改为原先的扫描路径,同时也是swagger的扫描路径
mvc:
pathmatch:
matching-strategy: ant_path_matcher

只有在dev和test环境才能进入swagger

Profiles of = Profiles.of("dev", "test");//监听是否存在某个信息
boolean flag = environment.acceptsProfiles(of);//是否允许在浏览器访问swagger
docket.enable(flag);

分组

不同的组开发不同的接口

//多个组
@Bean
public Docket docket1(){return new Docket(DocumentationType.SWAGGER_2).groupName("望江");
}@Bean
public Docket docket2(){return new Docket(DocumentationType.SWAGGER_2).groupName("长歌");
}

接口(请求)的返回值如果是对象,就会直接存入swagger中

@ApiOperation("测试用户控制类的方法")
@GetMapping("/test/UserSwagger")
@ResponseBody//swagger测试时要求访问json数据
public Student testUserSwagger(@Param("用户信息测试")Student user){return user;
}

给模块和属性加注释

@ApiOperation("给方法注释")
@GetMapping("/test/UserSwagger")
@ResponseBody//swagger测试时要求访问json数据
public Student testUserSwagger(@Param("给传参注释")Student user){return user;
}

给实体类和属性写注释

package com.changGe.li.pojo;import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;@Api("用户登录控制类")
@ApiModel("给类注释")
@Data
@AllArgsConstructor
public class Student {@ApiModelProperty("类中的属性")private String name;}

注意:项目正式发布时,一定要关闭swagger,不要 让用户知道你的所有接口


任务

异步任务

@Async//告诉spring这是一个异步任务

@Async//告诉spring这是一个异步任务
@RequestMapping("/jsp/register")
@ResponseBody
public String register(@RequestParam(value = "email") @Email String takeOver,@RequestParam(value = "textarea",required = false) String context,Model model) throws Exception{//多线程发送验证码Callable callable = new Inner(takeOver,context);String verifyCode = (String)callable.call();return verifyCode;
}

启动类上开启异步任务功能

//开启异步任务
@EnableAsync
@SpringBootApplication
@MapperScan(basePackages = "com.changGe.li.mappers")
public class Application {

复杂邮件发送

进入MailSenderAutoConfig,通过注解等,可以看到如mailProperties类等

MimeMessage mimeMessage = mailSender.createMimeMessage();try {//复杂邮件对象,开启附件MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);//附件helper.addAttachment("长歌.jpg",new File("F:\\picture\\2053237.jpg"));helper.setTo(takeOver);helper.setFrom("3276295265@qq.com");//邮件主题和内容helper.setSubject("li.changGe.com注册验证码");//允许html格式发送helper.setText("<p style='color:red'>您好验证码是:</p>"+ code + context,true);
} catch (MessagingException e) {e.printStackTrace();
}//这个类可以用来设置一些基本属性,如发送方,端口号等
mailSender.setUsername("3276295265@qq.com");
//发送邮件
mailSender.send(mimeMessage);
}

如果出现405,就把form请求改为get方式

<form method="get" onsubmit="sendEmail()"></form>

创造,创造,创造

定时任务

TaskScheduler任务调度者

TaskExecutor任务执行者

//开启定时功能
@EnableScheduling
@SpringBootApplication
public class Application {
//定时:取模10 分 时 日 月 一星期中的每一天
//只能在无参数的方法上
@Scheduled(cron = "0/10 * * * * 0-7")
public void test(){System.out.println("定时测试");
}

ZooKepper:服务注册与发现+Dubbo:专注于RPC解决方案

  • zookepper可以用来托管Dubbo

  • 分布式系统:若干独立计算机集合,对用户来说就像单个系统.

  • RPC:远程调用;两大;两大主要模块:通信+序列化

dubbo运行流程

安装zookepper和dubbo-admin

zookeeper注册中心: Apache Downloads

  1. 下载解压,双击bin下的zkServer.cmd,如果报错,就在conf目录下,新建一个zoo.cig文件.客户端是zkCl.cmd,记住zookepper的默认端口号是2181

  2. dubbo-admin监控管理后台:下载解压后,在D:\dubbo-admin-master-0.2.0下运行:mvn clean package -Dmaven.test.skip=true(清理并打包,同时跳过测试)

  3. 然后D:\dubbo-admin-master-0.2.0\dubbo-admin\target下会生成一个.jar包

  4. 启动zookepper服务的情况下,运行这个 jar包:java -jar .\dubbo-admin-0.0.1-SNAPSHOT.jar,浏览器访问7001,账号和密码都是root


HelloDubbo

SpringBoot整合Dubbo + zookeeper

  1. 总共三个模块:api模块,服务提供者和服务消费者
  2. 提供者和消费者需要导入api模块的依赖,这样就可以使用api模块中的类了
  3. 然后提供者和消费者都在yml配置文件中配置自己的注册名,注册中心地址和需要扫描的服务(只有提供者需要配置)
  4. 提供者实现api模块中的接口,注意加上**@Service(dubbo包下的)**和@Component(还是需要spring来接管的)
  5. 在启动zookepper服务的情况下,消费者通过**@Reference来代替@Autowired,实现从远程自动注入**提供者api服务.接着就可以使用api中的服务了

api公共接口

package com.changGe.shi.services;public interface UserService {String run();}

消费者和提供者需要在主启动类配置@EnableDubbo

<dependency><groupId>com.changGe.shi</groupId><artifactId>api</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency><dependency><groupId>com.alibaba.boot</groupId><artifactId>dubbo-spring-boot-starter</artifactId><version>0.2.0</version>
</dependency>
@SpringBootApplication
@EnableDubbo//@EnableDubbo等价于在配置文件中配置dubbo.scan.base-packages
public class ProviderApplication {
# 自己服务的名称,注册中心地址和扫描包
dubbo:# 设置服务应用名称 保证唯一application:name: providerregistry:# 指定注册中心address: zookeeper://127.0.0.1:2181# 指定通讯协议和端口 dubbo协议 默认端口20880protocol:port: 20880name: dubbo# 添加 monitor 监控monitor:address: registryscan:base-packages: com.changGe.shi.service
server:port: 8089

提供者实现api接口

package com.changGe.shi.service;import com.alibaba.dubbo.config.annotation.Service;
import com.changGe.shi.services.UserService;
import org.springframework.stereotype.Component;@Service//注意是dubbo包下的
@Component
public class ServiceImpl implements UserService {@Overridepublic String run() {return "run执行了";}}

消费者

dubbo:application:name: demo-consumerregistry:
address: zookeeper://127.0.0.1:2181
server:port: 8081
package com.changGe.shi.service;import com.alibaba.dubbo.config.annotation.Reference;
import com.changGe.shi.services.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;@Service
@Controller
public class Test {@Referenceprivate UserService userService;@RequestMapping("/test")@ResponseBodypublic String test(){return userService.run();}}

SpringBoot:快速使用Spring相关推荐

  1. Spring-boot快速实现Spring框架配置

    为什么80%的码农都做不了架构师?>>>    我们先来看一个非常简单的使用Spring boot的例子吧: 1. 我们创建一个Maven工程,假定工程名字为spring-boot, ...

  2. SpringBoot 2 整合 Spring Session 最简操作

    SpringBoot 2 整合 SpringSession 前言 Spring Session 介绍 SpringBoot 快速整合 Spring Session Spring Session 测试 ...

  3. 使用Spring Initializer快速创建Spring Boot项目

    使用Spring Initializer快速创建Spring Boot项目 1.IDEA:使用 Spring Initializer快速创建项目 IDE都支持使用Spring的项目创建向导快速创建一个 ...

  4. 使用 Spring Boot 快速构建 Spring 框架应用

    https://www.ibm.com/developerworks/cn/java/j-lo-spring-boot/index.html Spring 框架对于很多 Java 开发人员来说都不陌生 ...

  5. 使用 Spring Boot 快速构建 Spring 框架应用--转

    原文地址:https://www.ibm.com/developerworks/cn/java/j-lo-spring-boot/ Spring 框架对于很多 Java 开发人员来说都不陌生.自从 2 ...

  6. SpringBoot 快速开启事务(附常见坑点)

    SpringBoot 快速开启事务(附常见坑点) 序言:此前,我们主要通过XML配置Spring来托管事务.在SpringBoot则非常简单,只需在业务层添加事务注解(@Transactional ) ...

  7. SpringBoot快速构建项目

    我们再来看一下SpringBoot的快速构建项目,我们都是在集成的IDEA当中,创建一个maven project,在maven project的pom文件里呢,我们再去加SpringBoot相关的坐 ...

  8. @程序员,如何快速配置 Spring?

    作者 | 阿文,责编 | 郭芮 头图 | CSDN 下载自视觉中国 出品 | CSDN(ID:CSDNnews) 现在很多企业级的项目都是基于 spring 框架开发的,而这两年很火的微服务概念就有基 ...

  9. SpringBoot 快速集成 JWT 实现用户登录认证

    前言:当今前后端分离时代,基于Token的会话保持机制比传统的Session/Cookie机制更加方便,下面我会介绍SpringBoot快速集成JWT库java-jwt以完成用户登录认证. 一.JWT ...

最新文章

  1. 苹果自带的清理软件_清理苹果Mac系统垃圾用什么软件?
  2. 周四话分析:数据驱动,如何塑造下一个“教育领头羊”?
  3. Visual Studio Code Vue代码片段 总览
  4. jackson实现java对象转支付宝/微信模板消息
  5. python设置excel的格式_python使用xlrd与xlwt对excel的读写和格式设定
  6. oracle 数据库bak文件怎么打开,Oracle数据库的参数文件备份与恢复
  7. 经常玩电脑正确的坐姿_「姿态训练」保持良好坐姿的八个步骤
  8. 火山引擎智能容器云 veCompass v3.0 重磅发布!
  9. ni软件可以卸载吗_电视盒子自带的软件居然可以这样卸载!
  10. java parseint(12.0)_java的parseint
  11. 基于ADS54J60的JESD204B调试心得-fanfanStudio
  12. 文库文档网站大全,文档分享平台有哪些?
  13. 十年Smartbi项目经理:BI应用在银行业的发展历程和展望
  14. 精准测分:基于函数调用关系链的用例消振算法(上帝视角)
  15. 在真机测试遇到The executable was signed with invalid entitleme
  16. [Opencv]实验:实现窗宽窗位调节(附源码及解析)
  17. k2000显卡相当于gtx_电脑中的显卡是什么样干什么样的?NVDIA推出的两块Quadro显卡K1000M和K2000M性能究竟差多少...
  18. 4 Kubernetes资源-Pod控制器(2)
  19. 汉诺塔c语言做法:汉诺塔(Hanoi)是必须用递归方法才能解决的经典问题。它来自于印度神话。上帝创造世界时作了三根金刚石柱子,在第一根柱子上从下往上按大小顺序摞着64片黄金圆盘
  20. [机缘参悟-65]:《兵者,诡道也》-6-三十六计解读-并战计

热门文章

  1. 人像姿势,从细节做起!
  2. Driver class 'org.gjt.mm.mysql.Driver' could not be found, make sure the 'MySQL' driver (jar file)
  3. 请教一下水卡校验算法
  4. java对接天眼查接口,天眼查提供案例方法过期最新案例
  5. 【产品】项目管理的五个过程和九大知识领域
  6. 深度学习——卷积神经网络是否能编码位置信息?
  7. CJT长江连接器A2005系列线对板连接器排针排母PCB封装库
  8. uniapp中制作战力计算器
  9. macbook 安装win7
  10. 在函数前面加上WINAPI、CALLBACK等是什么意思