SpringBoot:快速使用Spirng
- 约定大于配置:很多配置springboot已经预设好了(和程序员达成约定);
- 特点:自动配置和起步依赖:依赖传递
spring解决企业级应用开发复杂性而创建的 --> 简化开发
原理
起步依赖
- 项目初始父级依赖spring-boot-dependencies,里面预定义了常用的依赖信息.
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-amqp</artifactId>
<version>${activemq.version}</version>
</dependency>
- 官方依赖初始依赖其他依赖,如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 {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public 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.factories
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;
}
开始分析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 elements
result.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容器中。
最终结论
- SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
- 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
run 方法做的的四件事
1、推断应用的类型是普通的项目还是Web项目
2、查找并加载所有可用初始化器 , 设置到initializers属性中
3、找出所有的应用程序监听器,设置到listeners属性中
4、推断并设置main方法的定义类,找到运行的主类
yaml(以数据为核心)语法
student:
name: 李长歌
age: 18
sex: 女
students: {name: 李世民,age: 20,sex: 男}
person:
- student
- teacher
- man
persons: [student,man,woman]
为属性赋值
@Value和Environment
@Value("${person.name}")
String name;
@Autowired
Environment 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_age
Name: ${person.name}李长歌
a_Age: 18
sex: 女
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: 8082
spring:
profiles: dev
---
student:
name: 3
server:
port: 8083
spring:
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,对应的路径是resourceProperties
addResourceHandler(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: /hello
web:
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.html
Resource 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;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Bean
public 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.网站图标请求处理程序
@Bean
public 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 {
//把自定义视图解析器加到容器中去
@Bean
public ViewResolver getViewResolver(){
return new MyViewResolver();
}
//自定义视图解析器,可以不做,用官方默认的不影响使用
class MyViewResolver implements ViewResolver{
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
}
//自定义自己的视图跳转链接
@Override
public 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)
动静分离:静态资源(不会改变数据)和动态模板(页面中的数据是会变化的)分开开发,可以理解成分开两个目录存放
引入时的版本号要比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.html
return "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: false
server:
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.html
return "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下的资源包login
basename: 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 {
@Override
public 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;
}
//存入地区,可以不实现
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {}
}
在我们自己的WebMvcConfigurer中声明国际化组件
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Bean
public 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 {
@Override
public 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 {
@Override
public 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: 100MB
max-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();
// 把文件的名称设置唯一值,uuid
String 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或587
port: 465
properties:
mail:
smtp:
auth: true #授权
socketFactory: ##SSL协议工厂获得SSL协议
class: javax.net.ssl.SSLSocketFactory
debug: true
protocol: 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;
}
@Override
public 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被创建时
@Override
public void sessionCreated(HttpSessionEvent se) {
//在最高的作用域servletContext操作session
ServletContext servletContext = se.getSession().getServletContext();
Integer online = (Integer) servletContext.getAttribute("online");
if(online == null || online <= 0){
online = 1;
}else {
online ++;
}
servletContext.setAttribute("online",online);
}
//有session被销毁时
@Override
public 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监控所有session
String 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: root
username: root
url: jdbc:mysql:///smbms?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=UTC
driver-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 {
@Autowired
DataSource dataSource;
@org.junit.jupiter.api.Test
void 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: root
username: root
url: jdbc:mysql:///smbms?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
#则导入log4j 依赖就行
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionoProperties: 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")
@Bean
public DataSource dataSource(){
return new DruidDataSource();
}
@Bean
public 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;
}
@Bean
public 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 {
@Override
protected 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;
@Resource
public void setUserMapper(UserMapper userMapper){
this.userMapper = userMapper;
}
private static HttpServletRequest httpServletRequest;
@Resource
public void setHttpServletRequest(HttpServletRequest httpServletRequest){
this.httpServletRequest = httpServletRequest;
}
@Override
public 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, stdout
log4j.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 {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected 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 {
//配置过滤器工厂
@Bean
public 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,这里写的是方法名
@Bean
public DefaultWebSecurityManager securityManager(@Qualifier("getRealm") Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
//配置Realm
@Bean
public 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 {
//多个组
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("望江");
}
//Environment:yml运行配置信息存储
//swagger的bean实例是docket
@Bean
public 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;
}
@Bean
public 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
- 下载解压,双击bin下的zkServer.cmd,如果报错,就在conf目录下,新建一个zoo.cig文件.客户端是zkCl.cmd,记住zookepper的默认端口号是2181
- dubbo-admin监控管理后台:下载解压后,在D:\dubbo-admin-master-0.2.0下运行:mvn clean package -Dmaven.test.skip=true(清理并打包,同时跳过测试)
- 然后D:\dubbo-admin-master-0.2.0\dubbo-admin\target下会生成一个.jar包
- 在启动zookepper服务的情况下,运行这个 jar包:java -jar .\dubbo-admin-0.0.1-SNAPSHOT.jar,浏览器访问7001,账号和密码都是root
HelloDubbo
SpringBoot整合Dubbo + zookeeper
- 总共三个模块:api模块,服务提供者和服务消费者
- 提供者和消费者需要导入api模块的依赖,这样就可以使用api模块中的类了
- 然后提供者和消费者都在yml配置文件中配置自己的注册名,注册中心地址和需要扫描的服务(只有提供者需要配置)
- 提供者实现api模块中的接口,注意加上**@Service(dubbo包下的)**和@Component(还是需要spring来接管的)
- 在启动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: provider
registry:
# 指定注册中心
address: zookeeper://127.0.0.1:2181
# 指定通讯协议和端口 dubbo协议 默认端口20880
protocol:
port: 20880
name: dubbo
# 添加 monitor 监控
monitor:
address: registry
scan:
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 {
@Override
public String run() {
return "run执行了";
}
}
消费者
dubbo:
application:
name: demo-consumer
registry:
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 {
@Reference
private UserService userService;
@RequestMapping("/test")
@ResponseBody
public String test(){
return userService.run();
}
}