SpringBoot随记
为什么叫随记呢
因为前面的笔记过于详细,从头到尾完整记下来会花掉很多时间…
so这篇就记得随意一点吧
Spring和SpringMVC都只是快速过了一下(跟没看一样 ^_^
Web容器基本配置
Tomcat配置
日志
HTTPS证书
生成秘钥复制到resource
目录下
keytool -genkey -alias myhttps -keyalg RSA -keysize 2048 -keystore zhengru_key.p12 -validity 365
在application.properties
中配置证书
server.ssl.key-alias=myhttps
server.ssl.key-store=classpath:zhengru_key.p12
server.ssl.key-store-password=123456
配置文件名称和路径
位置问题
位置可以在一下四个地方
当然也可以用spring.config.location
来指定其他位置
配置文件位置时,值一定要以/
结尾
例如:spring.config.location=classpath:/javaboy/
文件名问题
默认是叫application
但是也可以指定其他的文件名
用spring.config.name
来指定
例如:spring.config.name=app.properties
普通的属性注入
spring
里的东西,跟springboot
没有关系
使用Value
注解属性注入,数组也可以注入但是注意要用英文逗号隔开
这里注意要在设置中更改文件编码为
UTF-8
类型安全的属性注入
使用@ConfigurationProperties(prefix = "xxx")
设置前缀是xxx
,就会自动注入
@Component
@PropertySource("classpath:book.properties")
@ConfigurationProperties(prefix = "book")
public class Book {
private String name;
private String author;
private String[] tags;
//...
}
在配置文件中引用maven配置
需要使用@xxx@
,因为会和${}
冲突
短命令行参数
java -jar properties-0.0.1-SNAPSHOT.jar
改端口
java -jar properties-0.0.1-SNAPSHOT.jar --server.port=8081
YAML配置
Spring Boot 中的配置文件有两种格式,properties(默认)或者 yaml(yml也可)
属性注入
英文冒号后面需要加一个空格!
注意数组对象注入的形式
book:
name: 三国演义
tags:
- 历史
- 小说
- 明代
authors:
- name: 罗贯中
age: 88
- name: 施耐庵
age: 99
Profile问题
生产环境切换
spring.profiles.active=dev
spring.profiles.active=prod
spring.profiles.active=test
日志配置
SpringBoot默认的日志实现是Logback
详细配置可以看松哥的这篇文章 点此跳转
SpingBoot+Thymeleaf
松哥的文章 点此跳转
与传统Java模板引擎不同的是,Thymeleaf支持HTML原型
实践
创建项目时就可以添加依赖
# THYMELEAF (ThymeleafAutoConfiguration)
# 开启模板缓存(默认值: true )
spring.thymeleaf.cache=true
# 检查模板是否存在,然后再呈现
spring.thymeleaf.check-template=true
# 检查模板位置是否正确(默认值 :true )
spring.thymeleaf.check-template-location=true
#Content-Type 的值(默认值: text/html )
spring.thymeleaf.content-type=text/html
# 开启 MVC Thymeleaf 视图解析(默认值: true )
spring.thymeleaf.enabled=true
# 模板编码
spring.thymeleaf.encoding=UTF-8
# 要被排除在解析之外的视图名称列表,⽤逗号分隔
spring.thymeleaf.excluded-view-names=
# 要运⽤于模板之上的模板模式。另⻅ StandardTemplate-ModeHandlers( 默认值: HTML5)
spring.thymeleaf.mode=HTML5
# 在构建 URL 时添加到视图名称前的前缀(默认值: classpath:/templates/ )
spring.thymeleaf.prefix=classpath:/templates/
# 在构建 URL 时添加到视图名称后的后缀(默认值: .html )
spring.thymeleaf.suffix=.html
写一个UserController
@Controller
public class UserController {
@GetMapping("/hello")
public String index(Model model){
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User u = new User();
u.setId(i);
u.setUsername("zhengru:"+i);
u.setAddress("zhengru.top:"+i);
users.add(u);
}
model.addAttribute("users",users);
return "hello";
}
}
在template目录下创建hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1">
<tr th:each = "u : ${users}">
<td th:text = "${u.id}"></td>
<td th:text = "${u.username}"></td>
<td th:text = "${u.address}"></td>
</tr>
</table>
</body>
</html>
手动渲染
@SpringBootTest
class ThymeleafApplicationTests {
@Autowired
TemplateEngine templateEngine;
@Test
void contextLoads() {
Context ctx = new Context();
ctx.setVariable("username","邹川瑞");
ctx.setVariable("position","Java高级开发工程师");
ctx.setVariable("salary","600000");
String mail = templateEngine.process("mail", ctx);
System.out.println(mail);
}
}
表达式语法
说一下${}、*{}、#{}、@{}
${}
普通引用
*{}
可以实现以下操作
<div th:object="${user}">
<div th:text="*{username}"></div>
<div th:text="*{address}"></div>
<div th:text="*{id}"></div>
</div>
#{}
一般用于国际化
<div th:text="#{hello}"></div>
@{}
<script th:src="@{https://localhost:8080/hello.js}"></script>
<script th:src="@{~/hello.js}"></script>
<script th:src="@{//localhost:8080/hello.js}"></script>
<script th:src="@{//localhost:8080/hello.js(name='zhengru',age=99)}"></script>
<script th:src="@{/hello.js}"></script>
字面量
- 文本字面量:’one text’, ‘Another one!’,…
- 数字字面量:0, 34, 3.0, 12.3,…
- 布尔字面量:true, false
- Null字面量:null
- 字面量标记:one, sometext, main,…
如果文本是英文,并且不包含空格、逗号等字符,可以不用加单引号
文本运算
如果字符串中包含变量,也可以使用另一种简单的方式,叫做字面量置换,用 |
代替 '...' + '...'
,如下:
th:with 定义了一个局部变量 age,在其所在的 div 中可以使用该局部变量
布尔运算
- 二元运算符:and, or
- 布尔非(一元运算符):!, not
比较和相等
表达式里的值可以使用 >
, <
, >=
和 <=
符号比较。==
和 !=
运算符用于检查相等(或者不相等)。注意 XML
规定 <
和 >
标签不能用于属性值,所以应当把它们转义为 <
和 >
如果不想转义,也可以使用别名:gt (>);lt (<);ge (>=);le (<=);not (!)。还有 eq (==), neq/ne (!=)
条件运算符
类似于我们 Java 中的三目运算符
内置对象
基本内置对象:
- #ctx:上下文对象。
- #vars: 上下文变量。
- #locale:上下文区域设置。
- #request:(仅在 Web 上下文中)HttpServletRequest 对象。
- #response:(仅在 Web 上下文中)HttpServletResponse 对象。
- #session:(仅在 Web 上下文中)HttpSession 对象。
- #servletContext:(仅在 Web 上下文中)ServletContext 对象。
实用内置对象:
- #execInfo:有关正在处理的模板的信息。
- #messages:在变量表达式中获取外部化消息的方法,与使用#{…}语法获得的方式相同。
- #uris:转义URL / URI部分的方法
- #conversions:执行配置的转换服务(如果有)的方法。
- #dates:java.util.Date对象的方法:格式化,组件提取等
- #calendars:类似于#dates但是java.util.Calendar对象。
- #numbers:用于格式化数字对象的方法。
- #strings:String对象的方法:contains,startsWith,prepending / appending等
- #objects:一般对象的方法。
- #bools:布尔评估的方法。
- #arrays:数组方法。
- #lists:列表的方法。
- #sets:集合的方法。
- #maps:地图方法。
- #aggregates:在数组或集合上创建聚合的方法。
- #ids:处理可能重复的id属性的方法(例如,作为迭代的结果)。
设置属性值
<img th:attr="src=@{/1.png},title=${user.username},alt=${user.username}">
<img src="/myapp/1.png" title="javaboy" alt="javaboy">
<img th:src="@{/1.png}" th:alt="${user.username}" th:title="${user.username}">
<img th:src="@{/1.png}" th:alt-title="${user.username}">
遍历
<table border="1">
<tr th:each="u : ${users}">
<td th:text="${u.username}"></td>
<td th:text="${u.address}"></td>
</tr>
</table>
还有很多细节可以看松哥的文章
分支语句
th:if
th:unless
th:switch…
本地变量
这个我们前面已经涉及到了,使用 th:with 可以定义一个本地变量
内联
<div>hello [[${user.username}]]</div>
[[...]]
对应于 th:text (结果会是转义的 HTML)
[(...)]
对应于 th:text,它不会执行任何的 HTML 转义
<div th:with="str='hello <strong>javaboy</strong>'">
<div>[[${str}]]</div>
<div>[(${str})]</div>
</div>
显示效果
详细看松哥的文章 点此跳转
SpingBoot+Freemarker
文章地址 点此跳转
FreeMarkerProperties
中则配置了Freemarker
的基本信息,例如模板位置在 classpath:/templates/
,再例如模板后缀为 .ftl
,那么这些配置我们以后都可以在application.properties
中进行修改
Json
SpringMVC
框架中已经自动配置了jackson
和gson
的HTTPMessageConverter
只需要添加依赖就能使用
序列化:对象 -> JSON
(响应JSON)
反序列化:JSON -> 对象
(请求参数是JSON)
Jackson
指定属性序列化/反序列化时的名称
@JsonProperty("name")
private String username;
忽略字段
忽略某一个字段
@JsonIgnore
private Integer id;
忽略某些字段
@JsonIgnoreProperties({"id","username"})
public class User {
private Integer id;
private String username;
private String address;
}
日期格式化
局部配置
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private Date birthday;
全局配置
@Configuration
public class WebMvcConfig {
@Bean
ObjectMapper ObjectMapper(){
ObjectMapper om = new ObjectMapper();
om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
return om;
}
}
Gson
就放两个图因为我也没试过
一个是application.properties
另一个是WebMvcConfig
FastJson
需要自己配,可以在方法中或者是自己配一个bean
静态资源
一共有五个位置可以放置静态资源(优先级)
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
/
第五个用的比较少
自定义配置
application.properties
第一行配置表示定义资源位置,第二行配置表示定义请求 URL 规则
spring.web.resources.static-locations=classpath:/
spring.mvc.static-path-pattern=/**
Java代码定义
这里按照我的理解就是在用addResourceHandler
映射实际资源的位置也就是addResourceLocations
我可以通过/123/01.html
来访问/zhengru/01.html
这个资源
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/123/**").addResourceLocations("classpath:/zhengru/");
}
}
文件上传
单文件上传
写一个简单的页面
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit">
</form>
写一个controller
@RestController
public class FileUploadController {
SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");
@PostMapping("/upload")
public String upload(MultipartFile file, HttpServletRequest req){
String realPath = req.getServletContext().getRealPath("/");
String format = sdf.format(new Date());
String path = realPath + format;
File folder = new File(path);
if (!folder.exists()){
folder.mkdirs();
}
String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
try {
file.transferTo(new File(folder,newName));
String s = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + format + newName;
return s;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
}
可以在application.properties
中配置其他信息
比如限制单个文件的大小和限制所有文件的大小
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
多文件上传
合并多文件上传
<form action="/upload2" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple>
<input type="submit">
</form>
部分代码
try {
for (MultipartFile file : files) {
String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
file.transferTo(new File(folder,newName));
String s = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + format + newName;
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
独立多文件上传
<form action="/upload3" method="post" enctype="multipart/form-data">
<input type="file" name="file1">
<input type="file" name="file2">
<input type="file" name="file3">
<input type="submit">
</form>
public String upload(MultipartFile file1,MultipartFile file2,MultipartFile file3, HttpServletRequest req){
//...
}
Ajax上传
记得引入jQuery
<div id="result"></div>
<input type="file" id="file">
<input type="button" value="上传" onclick="uploadFile()">
<script language="JavaScript">
function uploadFile(){
var file = $("#file")[0].files[0];
var formData = new FormData();
formData.append("file",file);
$.ajax({
type:'post',
url:'/upload',
processData:false,
contentType:false,
data:formData,
success:function (msg) {
$("#result").html(msg)
}
})
}
</script>
ControllerAdvice注解的使用
全局异常处理
//@ControllerAdvice
@RestControllerAdvice
public class MyGlobalException {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ModelAndView customException(MaxUploadSizeExceededException e) throws IOException {
ModelAndView mv = new ModelAndView("myerror");
mv.addObject("error",e.getMessage());
return mv;
}
}
全局数据绑定
ModelAttribute
后面用到了ModelAttribute
注解,是MVC里的内容,先学一下
主要有两个作用
- 在数据回显时,给变量定义别名
- 定义全局数据
当用户访问当前Controller中的任意一个方法,在返回数据时,都会将添加了@ModelAttribute注解的方法的返回值,一起返回给前端
定义全局数据
@ControllerAdvice
public class MyGlobalData {
@ModelAttribute("info")
public Map<String,String> mydata(){
Map<String,String> info = new HashMap<>();
info.put("username","dzr");
info.put("address","zhengru.top");
return info;
}
}
@RestController
public class HelloController {
@GetMapping("/hello")
public void hello(Model model){
Map<String,Object> asMap = model.asMap();
Map<String,String> info = (Map<String,String>) asMap.get("info");
Set<String> keySet = info.keySet();
for (String s : keySet) {
System.out.println(s + "————" + info.get(s));
}
}
}
全局数据预处理
@ControllerAdvice
public class MyGlobalData {
@InitBinder("a")
public void a(WebDataBinder binder){
binder.setFieldDefaultPrefix("a.");
}
@InitBinder("b")
public void b(WebDataBinder binder){
binder.setFieldDefaultPrefix("b.");
}
}
@RestController
public class BookController {
@PostMapping("/book")
public void addBook(@ModelAttribute("b") Book book,@ModelAttribute("a") Author author){
System.out.println(book);
System.out.println(author);
}
}
关于异常
异常页面定义
404问题自动寻找异常页面的优先级,其他同理
- templates/error/404.html
- static/error/404.html
- templates/error/4xx.html
- static/error/4xx.html
使用动态显示异常
<table border="1px">
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
</table>
自定义异常
@Component
public class MyErrorAtributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
if ((Integer) map.get("status") == 404){
map.put("message","页面不存在");
}
return map;
}
}
跨域问题
第一种方式可以使用@CrossOrigin注解来让某一个类或某一个方法接收某个域的请求
@CrossOrigin(value = "http://localhost:8081/",maxAge = 1800)
第二种方式可以使用全局配置来一次性解决
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedHeaders("*")
// .allowedMethods("*")
// .allowedOrigins("http://localhost:8081/")
// .maxAge(1800);
// }
@Bean
CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration cfg = new CorsConfiguration();
cfg.addAllowedOrigin("http://localhost:8081");
cfg.addAllowedMethod("*");
source.registerCorsConfiguration("/**",cfg);
return new CorsFilter(source);
}
}
拦截器
preHandle
- 返回false,请求将不再继续执行
postHandle
- Controller执行之后被调用
afterCompletion
- preHandle返回true才会执行
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
System.out.println("postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
System.out.println("afterCompletion");
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/hello");
}
}
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello1";
}
@GetMapping("hello2")
public String hello2(){
return "hello2";
}
}
系统启动任务
CommandLineRunner
@Component
@Order(100)
public class MyCommandLineRunner01 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("args1 = " + Arrays.toString(args));
}
}
@Component
@Order(99)
public class MyCommandLineRunner02 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("args2 = " + Arrays.toString(args));
}
}
ApplicationRunner
两个用法差异不大,主要体现在对参数的处理上 ,ApplicationRunner
的参数类型更加丰富一些
@Component
@Order(98)
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
List<String> nonOptionArgs = args.getNonOptionArgs();
System.out.println("nonOptionArgs = " + nonOptionArgs);
Set<String> optionNames = args.getOptionNames();
for (String optionName : optionNames) {
System.out.println(optionName + "-->" + args.getOptionValues(optionName));
}
String[] sourceArgs = args.getSourceArgs();
System.out.println("sourceArgs = " + Arrays.toString(sourceArgs));
}
}
整合Web组件
Servlet
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("MyServlet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
Filter
@WebFilter("/*")
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("MyFilter");
filterChain.doFilter(servletRequest,servletResponse);
}
}
Listener
@WebListener
public class MyListener extends RequestContextListener {
@Override
public void requestInitialized(ServletRequestEvent requestEvent) {
System.out.println("requestInitialized");
}
@Override
public void requestDestroyed(ServletRequestEvent requestEvent) {
System.out.println("requestDestroyed");
}
}
扫描
@SpringBootApplication
@ServletComponentScan("top.zhengru.webcomponent") //记得加
public class WebcomponentApplication {
public static void main(String[] args) {
SpringApplication.run(WebcomponentApplication.class, args);
}
}
注册过滤器
除了用WebFilter
和Component
注解以外,还可以用Configuration
并且这个方法能够解决无法同时指定拦截路径和优先级的问题
@Configuration
public class FilterConfiguration {
@Bean
FilterRegistrationBean<MyFilter04> filter04FilterRegistrationBean04(){
FilterRegistrationBean<MyFilter04> bean = new FilterRegistrationBean<>();
bean.setOrder(90);
bean.setFilter(new MyFilter04());
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
@Bean
FilterRegistrationBean<MyFilter05> filter05FilterRegistrationBean05(){
FilterRegistrationBean<MyFilter05> bean = new FilterRegistrationBean<>();
bean.setOrder(89);
bean.setFilter(new MyFilter05());
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
}
参数类型转换
如果是key/value
的形式传入的参数,会使用自定义类型转换器处理(比如下面的MyDateConverter)
如果是传入json
则可以使用@RequestBody
注解,不需要类型转换器,通过HttpMessageConverter
转换为对象
@RestController
public class UserController {
@PostMapping("/user")
public void addUser(User user){
System.out.println(user);
}
@PostMapping("/user2")
public void addUser2(@RequestBody User user){
System.out.println(user);
}
}
@Component
public class MyDateConverter implements Converter<String,Date> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@Override
public Date convert(String source) {
try {
return sdf.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
项目首页和角标
优先找静态然后再找动态
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("index");
}
}
角标就在那五个地方放一个favicon.ico
即可,不需要其他的配置
AOP
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* top.zhengru.aop.Service.*.*(..))")
public void pc1(){
}
@Before("pc1()")
public void before(JoinPoint jp){
String name = jp.getSignature().getName();
System.out.println(name + "方法开始执行了");
}
@After("pc1()")
public void after(JoinPoint jp){
String name = jp.getSignature().getName();
System.out.println(name + "方法结束了");
}
@AfterReturning(value = "pc1()",returning = "s")
public void afterReturning(JoinPoint jp,String s){
String name = jp.getSignature().getName();
System.out.println(name + "方法返回值是 " + s);
}
@AfterThrowing(value = "pc1()",throwing = "e")
public void afterThrowing(JoinPoint jp,Exception e){
String name = jp.getSignature().getName();
System.out.println(name + "方法抛出了异常 " + e);
}
@Around("pc1()")
public Object around(ProceedingJoinPoint pjp){
try {
Object proceed = pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
}
JdbcTemplate
用GeneratedKeyHolder
来获取主键
@Service
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate;
public int addUser(User user){
int result = jdbcTemplate.update("insert into user (username,address) values (?,?)",user.getUsername(),user.getAddress());
return result;
}
public int addUser2(User user){
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
int res = jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement ps = con.prepareStatement("insert into user (username,address) values (?,?)", Statement.RETURN_GENERATED_KEYS);
ps.setString(1,user.getUsername());
ps.setString(2,user.getAddress());
return ps;
}
},keyHolder);
user.setId(keyHolder.getKey().longValue());
return res;
}
public List<User> getAllUser(){
List<User> list = jdbcTemplate.query("select * from user", new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
String username = rs.getString("username");
String address = rs.getString("address");
long id = rs.getLong("id");
User user = new User();
user.setId(id);
user.setUsername(username);
user.setAddress(address);
return user;
}
});
return list;
}
}
配置多数据源
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.one")
DataSource dsOne() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.two")
DataSource dsTwo() {
return DruidDataSourceBuilder.create().build();
}
}
@Configuration
public class JdbcTemplateConfig {
@Bean
JdbcTemplate jdbcTemplateOne(@Qualifier("dsOne") DataSource dsOne) {
return new JdbcTemplate(dsOne);
}
@Bean
JdbcTemplate jdbcTemplateTwo(@Qualifier("dsTwo") DataSource dsTwo) {
return new JdbcTemplate(dsTwo);
}
}
MyBatis
可以在MybatisApplication
前面加上@MapperScan
注解全局配置扫描Mapper
其他的地方都差不多
@Autowired
UserMapper userMapper;
用XML
的话可以在application.properties
中配置mapper
文件的目录
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mappers/*xml
配置多数据源
有点麻烦,下面是松哥的pdf截图
Redis
redis
还没学就先简单看了一下
@SpringBootTest
class RedisApplicationTests {
@Autowired
RedisTemplate redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
void contextLoads() {
User user = new User();
user.setUsername("dzr");
user.setAddress("zhengru.top");
ValueOperations ops = redisTemplate.opsForValue();
ops.set("u",user);
User u = (User) ops.get("u");
System.out.println(u);
}
@Test
void test1(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("dzr","zhengru.top");
String dzr = ops.get("dzr");
System.out.println("dzr = " + dzr);
}
}
Session共享
分别在8080
和8081
端口开启服务,发现8081
可以共享8080
的session
,因为Spring Session
就是使用 Spring
中的代理过滤器,将所有的 Session
操作拦截下来,自动的将数据 同步到 Redis
中,或者自动的从 Redis
中读取数据
@RestController
public class HelloController {
@Value("${server.port}")
Integer port;
@GetMapping("/set")
public String set(HttpSession session){
session.setAttribute("dzr","zhengru.top");
return String.valueOf(port);
}
@GetMapping("/get")
public String get(HttpSession session){
String dzr = (String) session.getAttribute("dzr");
return dzr + ":" + port;
}
}
Redis处理接口幂等性
通过拦截器,检测有没有@AutoIdempotent
注解,如果有则checkToken
,redis
中没有token
就返回token
不存在,相应的,如果先前已经生成了token
,就返回hello
这里放出核心的部分
@Service
public class RedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
public boolean setEx(String key,String value,Long expireTime){
boolean result = false;
try {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set(key,value);
stringRedisTemplate.expire(key,expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e){
e.printStackTrace();
}
return result;
}
public boolean exists(String key){
return stringRedisTemplate.hasKey(key);
}
public boolean remove(String key){
if (exists(key)){
return stringRedisTemplate.delete(key);
}
return false;
}
}
public class TokenService {
@Autowired
RedisService redisService;
public String createToken(){
String uuid = UUID.randomUUID().toString();
redisService.setEx(uuid,uuid,10000L);
return uuid;
}
public boolean checkToken(HttpServletRequest request) throws IdempotentException {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)){
token = request.getParameter("token");
if (StringUtils.isEmpty(token)){
throw new IdempotentException("Token不存在");
}
}
if (!redisService.exists(token)){
throw new IdempotentException("重复操作");
}
boolean remove = redisService.remove(token);
if (!remove) {
throw new IdempotentException("重复操作");
}
return true;
}
}
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
@Autowired
TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)){
return true;
}
Method method = ((HandlerMethod) handler).getMethod();
AutoIdempotent idempotent = method.getAnnotation(AutoIdempotent.class);
if (idempotent != null) {
try {
return tokenService.checkToken(request);
} catch (IdempotentException e){
throw e;
}
}
return true;
}
}
RESTful
在SpringBoot中构建RESTful非常容易
用法
查询
get
http://localhost:8080/users/1
添加
post/json添加数据
http://localhost:8080/users
修改
put/json
http://localhost:8080/users/8
分页查询
http://localhost:8080/users?page=0&size=3&sort=id,desc
实现
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String address;
//...
}
public interface UserDao extends JpaRepository<User,Long> {
}
Spring Cache
@SpringBootApplication
@EnableCaching //开启缓存功能
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
@CacheConfig
这个注解在类上使用,用来描述该类中所有方法使用的缓存名称,当然也可以不使用该注解,直接在具体的缓存注解上配置名称
@Service
@CacheConfig(cacheNames = "c1")
public class UserService {
}
@Cacheable
这个注解一般加在查询方法上,表示将一个方法的返回值缓存起来,默认情况下,缓存的key就是方法的参数,缓存的value就是方法的返回值
@Cacheable(key = "#id")
public User getUserById(Integer id,String username) {
System.out.println("getUserById");
return getUserFromDBById(id);
}
@CachePut
这个注解一般加在更新方法上,当数据库中的数据更新后,缓存中的数据也要跟着更新,使用该注解,可以将方法的返回值自动更新到已经存在的key上
@CachePut(key = "#user.id")
public User updateUserById(User user) {
return user;
}
@CacheEvict
这个注解一般加在删除方法上,当数据库中的数据删除后,相关的缓存数据也要自动清除,该注解在使用的时候也可以配置按照某种条件删除(condition属性)或者或者配置清除所有缓存(allEntries属性)
@CacheEvict()
public void deleteUserById(Integer id) {
//在这里执行删除操作, 删除是去数据库中删除
}
SpringSecurity
这里的安全内容准备单独写一篇,所以先跳过了
WebSocket
下面是使用WebSocket
实现的一个聊天室(多人)
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
}
}
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Message greeting(Message message){
return message;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="/webjars/jquery/3.5.1/dist/jquery.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
</head>
<body>
<div>
<label for="username">请输入用户名:</label>
<input type="text" id="username" placeholder="用户名">
</div>
<div>
<input type="button" value="连接" id="connect">
<input type="button" value="断开连接" id="disconnect" disabled="disabled">
</div>
<div id="chat">
</div>
<div>
<label for="content">请输入聊天内容:</label>
<input type="text" id="content" placeholder="聊天内容">
</div>
<button id="send">发送</button>
<script>
var stompClient;
$(function (){
$("#connect").click(function (){
connect();
$("#send").click(function (){
stompClient.send("/hello",{},JSON.stringify({'name':$("#username").val(),'content':$("#content").val()}))
})
})
})
function connect(){
if (!$("#username").val()){
return;
}
var soketjs = new SockJS("/chat");
stompClient = Stomp.over(soketjs);
stompClient.connect({},function (frame){
setConnect(true)
stompClient.subscribe("/topic/greetings",function (greeting){
var msgContent = JSON.parse(greeting.body);
$("#chat").append("<div>" + msgContent.name + ":" + msgContent.content + "</div>")
})
})
}
function setConnect(connected){
$("#connect").prop("disabled",connected);
$("#disconnect").prop("disabled",!connected)
}
</script>
</body>
</html>
ActiveMQ
spring.activemq.broker-url=tcp://127.0.0.1:61616
spring.activemq.packages.trust-all=true
spring.activemq.user=admin
spring.activemq.password=admin
@SpringBootApplication
public class JmsApplication {
public static void main(String[] args) {
SpringApplication.run(JmsApplication.class, args);
}
@Bean
Queue queue(){
return new ActiveMQQueue("dzr-queue");
}
}
@Component
public class JmsComponent {
@Autowired
JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
Queue queue;
public void send(Message message){
jmsMessagingTemplate.convertAndSend(this.queue,message);
}
@JmsListener(destination = "dzr-queue")
public void receive(Message msg){
System.out.println("msg = " + msg);
}
}
@SpringBootTest
class JmsApplicationTests {
@Autowired
JmsComponent jmsComponent;
@Test
void contextLoads() {
Message message = new Message();
message.setContent("hello dzr");
message.setDate(new Date());
jmsComponent.send(message);
}
}
RabbitMQ
Direct模式
这里注释的部分可以省略
@Configuration
public class DirectConfig {
@Bean
Queue directQueue(){
return new Queue("dzr-queue");
}
// @Bean
// DirectExchange directExchange(){
// return new DirectExchange("dzr-direct",true,false);
// }
//
// @Bean
// Binding directBinding(){
// return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
// }
}
@Component
public class DirectReceiver {
@RabbitListener(queues = "dzr-queue")
public void handler(String msg){
System.out.println("msg = " + msg);
}
}
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("dzr-queue","hello,dzr!");
}
Fanout模式
@Configuration
public class FanoutConfig {
@Bean
Queue queueOne(){
return new Queue("queue-one");
}
@Bean
Queue queueTwo(){
return new Queue("queue-two");
}
@Bean
FanoutExchange fanoutExchange(){
return new FanoutExchange("dzr",true,false);
}
@Bean
Binding bindingOne(){
return BindingBuilder.bind(queueOne()).to(fanoutExchange());
}
@Bean
Binding bindingTwo(){
return BindingBuilder.bind(queueTwo()).to(fanoutExchange());
}
}
@Component
public class FaoutReceiver {
@RabbitListener(queues = "queue-one")
public void handler1(String msg){
System.out.println("msg = " + msg);
}
@RabbitListener(queues = "queue-two")
public void handler2(String msg){
System.out.println("msg = " + msg);
}
}
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("dzr-fanout",null,"hello dzr!");
}
Topic模式
@Configuration
public class TopicConfig {
@Bean
Queue xiaomi(){
return new Queue("xiaomi");
}
@Bean
Queue huawei(){
return new Queue("huawei");
}
@Bean
Queue phone(){
return new Queue("phone");
}
@Bean
TopicExchange topicExchange(){
return new TopicExchange("dzr-topic",true,false);
}
@Bean
Binding xiaomiBinding(){
return BindingBuilder.bind(xiaomi()).to(topicExchange()).with("xiaomi.#");
}
@Bean
Binding huaweiBinding(){
return BindingBuilder.bind(huawei()).to(topicExchange()).with("huawei.#");
}
@Bean
Binding phoneBinding(){
return BindingBuilder.bind(phone()).to(topicExchange()).with("#.phone.#");
}
}
@Component
public class TopicReceiver {
@RabbitListener(queues = "phone")
public void handler1(String msg){
System.out.println("msg:phone = " + msg);
}
@RabbitListener(queues = "xiaomi")
public void handler2(String msg){
System.out.println("msg:xiaomi = " + msg);
}
@RabbitListener(queues = "huawei")
public void handler3(String msg){
System.out.println("msg:huawei = " + msg);
}
}
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("dzr-topic","xiaomi.news","小米新闻");
rabbitTemplate.convertAndSend("dzr-topic","xiaomi.phone","小米手机");
rabbitTemplate.convertAndSend("dzr-topic","huawei.phone","华为手机");
}
Header模式
用的比较少,节约篇幅就不放代码了
下面的username
就是发件人邮箱,password
即POP3/SMTP
协议的授权码(不是邮箱密码!)
spring.mail.host=smtp.163.com
spring.mail.port=25
spring.mail.username=***
spring.mail.password=***
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.debug=true
contextLoads
是普通邮件,test1
是附件邮件,test2
是图片资源邮件,test3
是发送freemaker
模板
package top.zhengru.mail;
import freemarker.core.Configurable;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import top.zhengru.mail.model.User;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Date;
@SpringBootTest
class MailApplicationTests {
@Autowired
JavaMailSender javaMailSender;
@Test
void contextLoads() {
SimpleMailMessage simMsg = new SimpleMailMessage();
simMsg.setFrom("***");
simMsg.setTo("***");
simMsg.setSentDate(new Date());
simMsg.setSubject("邮件主题-测试邮件");
simMsg.setText("邮件内容-测试邮件");
javaMailSender.send(simMsg);
}
@Test
void test1() throws MessagingException {
File file = new File("C:/Users/董政儒/Desktop/头像.jpg");
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("***");
helper.setTo("***");
helper.setSentDate(new Date());
helper.setSubject("邮件主题-测试邮件");
helper.setText("邮件内容-测试邮件");
helper.addAttachment(file.getName(),file);
javaMailSender.send(mimeMessage);
}
@Test
void test2() throws MessagingException {
File file = new File("C:/Users/董政儒/Desktop/头像.jpg");
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("***");
helper.setTo("***");
helper.setSentDate(new Date());
helper.setSubject("邮件主题-测试邮件");
helper.setText("<div>hello,这是一封带图片资源的邮件...</div><div><img src='cid:p01' /></div>",true);
helper.addInline("p01",file);
javaMailSender.send(mimeMessage);
}
@Test
void test3() throws MessagingException, IOException, TemplateException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom("***");
helper.setTo("***");
helper.setSentDate(new Date());
helper.setSubject("邮件主题-测试邮件");
Configuration cfg = new Configuration(Configuration.getVersion());
cfg.setClassLoaderForTemplateLoading(MailApplicationTests.class.getClassLoader(),"mail");
Template template = cfg.getTemplate("mail.ftl");
User user = new User();
user.setUsername("***");
user.setCompany("***");
user.setPosition("***");
user.setSalary(8000.0);
StringWriter out = new StringWriter();
template.process(user,out);
helper.setText(out.toString(), true);
javaMailSender.send(mimeMessage);
}
}
定时任务
@Scheduled
记得在@SpringBootApplication
下加@EnableScheduled
fixedDelay
- 控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次
fixedRate
- 是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行
initialDelay
- 如:@Scheduled(initialDelay = 10000,fixedRate = 15000,这个定时器就是在上一个的基础上加了一个initialDelay = 10000 意思就是在容器启动后,延迟10秒后再执行一次定时器,以后每15秒再执行一次该定时器
cron
- 表达式可以定制化执行任务,但是执行的方式是与fixedDelay相近的,也是会按照上一次方法结束时间开始算起
@Component
public class MySchedule {
@Scheduled(fixedDelay = 1000)
public void fixedDelay(){
System.out.println("fixedDelay:" + new Date());
}
@Scheduled(fixedRate = 1000)
public void fixedRate(){
System.out.println("fixedRate:" + new Date());
}
@Scheduled(initialDelay = 1000,fixedRate = 1000)
public void initDelay(){
System.out.println("initDelay:" + new Date());
}
@Scheduled(cron = "0/5 * * * * *")
public void cron(){
System.out.println("cron:" + new Date());
}
}
@Quartz
@Component
public class MyJob01 {
public void sayHello(){
System.out.println("MyJob01:" + new Date());
}
}
@Component
public class MyJob02 extends QuartzJobBean {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("MyJob02:" + name + ":" + new Date());
}
}
@Configuration
public class QuartzConfig {
@Bean
MethodInvokingJobDetailFactoryBean jobDetail01(){
MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
bean.setTargetBeanName("myJob01");
bean.setTargetMethod("sayHello");
return bean;
}
@Bean
JobDetailFactoryBean jobDetail02(){
JobDetailFactoryBean bean = new JobDetailFactoryBean();
bean.setJobClass(MyJob02.class);
JobDataMap map = new JobDataMap();
map.put("name","dzr");
bean.setJobDataMap(map);
return bean;
}
@Bean
SimpleTriggerFactoryBean simpleTriggerFactoryBean(){
SimpleTriggerFactoryBean bean = new SimpleTriggerFactoryBean();
bean.setJobDetail(jobDetail01().getObject());
bean.setRepeatCount(3);
bean.setStartDelay(1000);
bean.setRepeatInterval(1000);
return bean;
}
@Bean
CronTriggerFactoryBean cronTriggerFactoryBean(){
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
bean.setJobDetail(jobDetail02().getObject());
bean.setCronExpression("0/5 * * * * ?");
return bean;
}
@Bean
SchedulerFactoryBean schedulerFactoryBean(){
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setTriggers(simpleTriggerFactoryBean().getObject(),cronTriggerFactoryBean().getObject());
return bean;
}
}
Swagger
第一次跑的时候报错了,查了一下Swagger3.0
不兼容SpringBoot2.6.x
及以上的版本,于是就把SpringBoot
版本降到了2.5.6
建议还是使用Swagger2.x
?
- @ApiIgnore
- 忽略这个接口
- @ApiResponses
- 响应码描述
- @ApiOperation
- 接口名称及描述
- @ApiImplicitParams
- 参数类型、描述
@RestController
public class UserController {
@GetMapping("/hello")
@ApiIgnore
public String hello(){
return "hello";
}
@ApiResponses({
@ApiResponse(responseCode = "200",description = "请求成功"),
@ApiResponse(responseCode = "500",description = "请求失败")
})
@GetMapping("/user/{id}")
@ApiOperation(value = "查询用户",notes = "根据id查询用户")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "body",name = "id",value = "用户 id",required = true)
})
public String getUserById(@PathVariable Integer id){
return "user:" + id;
}
@PostMapping("/user")
@Parameter(name = "user")
public String addUser(@RequestBody User user){
return user.toString();
}
}
@ApiModel(value = "用户实体类",description = "这个类定义了用户所有的属性")
public class User {
@ApiModelProperty(value = "用户ID")
private Long id;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "用户地址")
private String address;
//...
}
数据校验
- @Null 被注解的元素必须为 null
- @NotNull 被注解的元素必须不为 null
- @AssertTrue 被注解的元素必须为 true
- @AssertFalse 被注解的元素必须为 false
- @Min(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
- @Max(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
- @DecimalMin(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
- @DecimalMax(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
- @Size(max=, min=) 被注解的元素的大小必须在指定的范围内
- @Digits (integer, fraction) 被注解的元素必须是一个数字,其值必须在可接受的范围内
- @Past 被注解的元素必须是一个过去的日期
- @Future 被注解的元素必须是一个将来的日期
- @Pattern(regex=,flag=) 被注解的元素必须符合指定的正则表达式
- @NotBlank(message =) 验证字符串非 null,且长度必须大于0
- @Email 被注解的元素必须是电子邮箱地址
- @Length(min=,max=) 被注解的字符串的大小必须在指定的范围内
- @NotEmpty 被注解的字符串的必须非空
- @Range(min=,max=,message=) 被注解的元素必须在合适的范围内
普通校验
public class User {
private Long id;
@Size(min = 2,max = 5,message = "{user.username.size}")
private String username;
@NotNull
private String address;
//...
}
这里的message
可以在resource
目录下创建一个ValidationMessages.properties
文件,在里面定义提示,例如:
user.username.size=username长度介于2-5
读取校验结果,BindingResult
里如果有信息,即校验失败
@RestController
public class UserController {
@PostMapping("/user")
public void addUser(@Validated User user, BindingResult result){
if (result != null && result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println("error.getDefaultMessage() = " + error.getDefaultMessage());
}
}else {
System.out.println(user.toString());
}
}
}
分组校验
创建两个接口然后就可以在校验条件注解里加入group
@Size(min = 2,max = 5,message = "{user.username.size}",groups = {ValidationGroup1.class})
private String username;
@NotNull(groups = {ValidationGroup1.class, ValidationGroup2.class})
private String address;
接着就可以在校验注解里加入分组
public void addUser(@Validated(ValidationGroup1.class) User user, BindingResult result){
//...
}
应用监控
先鸽一下…
打包问题
可执行jar和可依赖jar同时存在
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>
暂时就先到这,如果有补充会继续更新