在测试类中测试IoC容器的存在
- 添加注解
@ContextConfiguration(classes = CommunityApplication.class)
- 实现接口
ApplicationContextAware
- 重写方法
public void setApplicationContext(ApplicationContext applicationContext)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)//在测试类中加上此注解就能将配置类()引用在本类中
class CommunityApplicationTests implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Test
public void testApplicationContext(){
System.out.println(applicationContext);
//org.springframework.web.context.support.GenericWebApplicationContext@598bd2ba, started on Thu Jun 02 19:55:32 CST 2022
//证明容器是存在的
}
}
@Primary
注解在bean上表示优先被Ioc实例化
@PostConstruct
注解在方法上,表示在构造器运行之后执行
@PreDestory
注解在方法上,表示在销毁方法前执行
想实例化一个第三方jar包的bean:自己写个配置类,通过bean注解实现
@SpringbootApplication
一般用于程序入口的配置类
@Configuration
表示为一般配置类
@Configuration
public class AlphaConfig {
@Bean
public SimpleDateFormat simpleDateFormat(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}
@Test
public void testBeanConfiguration(){
SimpleDateFormat simpleDateFormat = applicationContext.getBean(SimpleDateFormat.class);
System.out.println(simpleDateFormat.format(new Date()));
//2022-06-02 20:13:55
}
第一种:
@RequestMapping(path = "/student",method = RequestMethod.GET)
@ResponseBody
public String getStudent(@RequestParam(name="current",required=false,defalutValue="1") int current,
@RequestParam(name="limit",required=false,defalutValue="10") int limit){}
RestFul
@RequestMapping(path = "/student/{id}",method = RequestMethod.GET)
@ResponseBody
public String getStudent(@PathVariable("id") int id){
System.out.println(id);
return "a student";
}
-
在sina开启授权码状态,和POP3,SMTP服务
-
新建工具类MailClient
/* 1.将其添加到springIoC管理 * 2.定义一个Logger,用于记录错误信息 * 3.将配置文件中的username注入,这是(代表了网站)发送方 * 4.定义sendMail方法,需要 发邮件的标题 ,内容 ,我的邮箱 ,他人的邮箱 四个参数 需要spring中的MimeMessageHelper 帮助构建邮件 */ @Component public class MailClient { private static final Logger logger = LoggerFactory.getLogger(MailClient.class); @Autowired private JavaMailSender mailSender; //需要发邮件的标题,内容,我的邮箱,他人的邮箱 //将username注入,因为服务器发邮件都是用直接的账号(配置中的sina) @Value("${spring.mail.username}") private String from; //封装公有方法 public void sendMail(String to, String subject, String content) { try { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message); helper.setFrom(from); helper.setTo(to); helper.setSubject(subject); helper.setText(content, true); mailSender.send(helper.getMimeMessage()); } catch (MessagingException e) { logger.error("发送邮件失败" + e.getMessage()); } } }
测试:
@Autowired
private MailClient mailClient;//注入工具类
@Test
public void testMail(){
mailClient.sendMail("574524709@qq.com","test","test mail");
}
需要发html形式邮箱:采用thymeleaf构建模板:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>邮件示例</title>
</head>
<body>
<p>欢迎您,<span style="color: darkorchid;" th:text="${username}"></span>!</p>
</body>
</html>
测试:
@Autowired
private TemplateEngine templateEngine;//springboot中已管理了模板引擎,只需注入
@Test
public void testHtmlMail(){
Context context = new Context();//注意是thymeleaf的类
context.setVariable("username","sunday");//这是其中的一个变量
String content = templateEngine.process("/mail/demo", context);//把模板地址,数据传入如
System.out.println(content);
mailClient.sendMail("574524709@qq.com","HTML",content);
}
启动出现问题
java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
在配置中数据库连接后加上
allowPublicKeyRetrieval=true
在首页点击(首页),(注册)都无页面
<!--注意thymeleaf的这个写法是错的-->
<a class="nav-link" th:href="@{site/index.html}">首页</a>
<!--采用这样-->
<a class="nav-link" th:href="@{index}">首页</a>
//Cookie示例
@RequestMapping(path = "/cookie/set",method = RequestMethod.GET)
@ResponseBody
public String setCookie(HttpServletResponse response){
Cookie cookie = new Cookie("code1", CommunityUtil.generateUUID());
//设置范围,有些路径下有效的
cookie.setPath("/community/alpha");
//生存时间(默认是关闭浏览器失效)
cookie.setMaxAge(600);//秒
//发送
response.addCookie(cookie);
return "set cookie";
}
@RequestMapping(path = "/cookie/get",method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code1") String code1){//原本在request中取得,但可以用注解取得并赋给值
System.out.println();
return "get cookie";
}
优点:存在服务器更安全
缺点:服务器压力
//Session是javaSE的规范,不是http的
@RequestMapping(path = "/session/set",method = RequestMethod.GET)
@ResponseBody
public String setSession(HttpSession session){//与cookie不同,springMVC会自动创建Session,只需要声明,就能注入进来
session.setAttribute("id",1);
session.setAttribute("name","test");
return "session test";
}
@RequestMapping(path = "/session/get",method = RequestMethod.GET)
@ResponseBody
public String getSession(HttpSession session){
System.out.println(session.getAttribute("id"));
System.out.println(session.getAttribute("name"));
return "get session test";
}
分布式部署:nginx实现负载均衡
- 粘性session:同一ip的请求均分配到指定一台服务器上
- 同步session:服务器将session同步给所有服务器
- 共享session:有一台单独的服务器用于处理session,其他服务器与该服务器
- 主流:不使用session,而是用cookie,部分不适合存cookie的存数据库里,数据库集群备份
- 更好的做法:不存在关系型数据库(硬盘)中,而是NOSQL中
Kaptcha:
- 导入jar包
- 编写kaptcha配置类
- 生成随机字符,图片
登录请求:
- 点击上方的“登入”,能跳到登入页面
- 点击“立即登入”,返回结果(登入凭证,cookie)发给客户端
退出请求:
- 将登入凭证修改为失效状态
- 跳转至首页
数据库中的表 login_ticket:
id | user_id | ticket | status | expired |
---|---|---|---|---|
随机字符串,唯一标识 | 0-有效 1-无效 | 过期时间 |
1、创建实体类,封装数据
//dao
/**
* 在本类中,学习使用注解实现sql,不是xml
*
*/
@Mapper
public interface LoginTicketMapper {
//登入成功后要插入凭证//需要声明主键自动生成,@Options,且需要将生成的值注入给对象,keyProperty = "id"
@Insert({
"insert into login_ticket (user_id,ticket,status,expired) ",//加个空格断开
"values(#{userId},#{ticket},#{status},#{expired})"
})
@Options(useGeneratedKeys = true,keyProperty = "id")
int insertLoginTicket(LoginTicket loginTicket);
//查询方法:围绕ticket
@Select({
"select id,user_id,ticket,status,expired ",
"from login_ticket where ticket=#{ticket}"
})
LoginTicket selectByTicket(String ticket);
//修改凭证状态:不删除
@Update({
"update login_ticket set status=#{status} where ticket=#{ticket}"
})
int updateStatus(String ticket,int status);
//学习:假如需要动态sql时
/*@Update({
"<script>",
"update login_ticket set status=#{status} where ticket=#{ticket} ",
"<if test=\"ticket!=null\">",
"and 1 =1",
"</if>",
"</script>"
})*/
}
//UserService
//实现登入功能:成功、失败(多种情况)
public Map<String ,Object> login(String username,String password,int expiredSeconds){
Map<String ,Object> map = new HashMap<>();
//空值判断
if(StringUtils.isBlank(username)){
map.put("usernameMsg","账号不能为空!");
return map;
}
if(StringUtils.isBlank(password)){
map.put("passwordMsg","密码不能为空!");
return map;
}
//验证合法性
User user = userMapper.selectByName(username);
if(user==null){
map.put("usernameMsg","账号不存在!");
return map;
}
//没激活的账号不能登入
if(user.getStatus()==0){
map.put("usernameMsg","账号未激活!");
return map;
}
//密码
password = CommunityUtil.md5(password+ user.getSalt());
if(user.getPassword().equals(password)){
map.put("passwordMsg","密码不正确!");
return map;
}
//登入成功,生成登入凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);//有效状态
loginTicket.setExpired(new Date(System.currentTimeMillis()+expiredSeconds*1000));
loginTicketMapper.insertLoginTicket(loginTicket);
//这个LoginTicket表就相当与session了,下次用户请求带上ticket,服务器查询状态和时间看是否有效
map.put("ticket",loginTicket.getTicket());
return map;
}
//LoginController
@RequestMapping(path = "/login",method = RequestMethod.POST)
public String login(String username,String password ,String code,boolean rememberme,//这个rememberme是勾选记住我
Model model,HttpSession session,HttpServletResponse response){//model用于范围响应数据;getKaptcha存的验证码需要session获取;登入成功了,需要将ticket发给客户端用cookie保存
String kaptcha = (String) session.getAttribute("kaptcha");
if(StringUtils.isBlank(kaptcha)||StringUtils.isBlank(code)||!kaptcha.equalsIgnoreCase(code)){
model.addAttribute("codeMsg","验证码不正确");
return "/site/login";
}
//检查账号密码
//如果勾选了“记住我”,则存的时间长一点
//这里再次在CommunityConstant类添加常量
int expiredSecond = rememberme?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSecond);
//如果map总包含ticket,就是成功了
if(map.containsKey("ticket")){
Cookie cookie = new Cookie("ticket",map.get("ticket").toString());
cookie.setPath(contextPath);//表示整个项目下cookie都是有效的:注入properties中的值
cookie.setMaxAge(expiredSecond);
response.addCookie(cookie);//将cookie发给用户
return "redirect:/index";
}else {
model.addAttribute("usernameMsg",map.get("usernameMsg"));
model.addAttribute("passwordMsg",map.get("passwordMsg"));
return "/site/login";
}
}
根据登入与否,调整头部信息,由于在整个网站都有:设定拦截器
1、定义拦截器
@Controller
public class AlphaInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle调用了:"+handler.toString());
return true;
}
//在Controller之后运行,模板引擎之前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("postHandle调用了:"+handler.toString());
}
//在模板引擎TemplateEngine之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterCompletion调用了:"+handler.toString());
}
//写完之后在config建立配置类
}
2、配置拦截器,指定、排除路径
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor).
excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg") //不拦截静态资源
.addPathPatterns("/register","/login"); //明确要拦截的路径:注册和登入
}
}
拦截器在本节的实现:
- 在请求开始时查询登入用户
- 在本次请求中持有的用户数据
- 在模板视图上显示用户数据
- 在结束请求时清理用户数据
上传头像和修改密码
上传文件:
- 请求:post
- 表单:enctype="multipart/form-data"
- SpringMVC :通过MultipartFile处理上传文件
步骤:
- 访问页面
- 上传头像
- 获取头像:在其他页面都要获取
1.Controller层
//LoginController
@RequestMapping(path = "/register",method = RequestMethod.GET)
public String getRegisterPage(){
return "/site/register";
}
2.修改register页面
3.引入CommonsLang包,用于常用字符串检测
4.写工具类(生成随机字符串,加密等),由于不需要交给容器托管,因此写成静态方法
//使用 自带的UUID包生成随机字符串
public static String generateUUID(){
return UUID.randomUUID().toString().replaceAll("-","");//不想要有横线
}
//加密密码:使用Spring 自带的工具类 DigestUtils.md5DigestAsHex()
public static String md5(String key){
if(StringUtils.isBlank(key)){//先简单判断下不为空,采用了commons lang包
return null;
}else {
return DigestUtils.md5DigestAsHex(key.getBytes(StandardCharsets.UTF_8));//这是spring自带的加密方法
}
}
5.开发注册业务
//UserService
//注入邮件客户端,模板引擎
//注入项目名,项目路径
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
//注册时发送激活码需要带上域名和项目名,因此从properties中注入
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
@Autowired
private LoginTicketMapper loginTicketMapper;
6.开发业务
返回结果:错误信息(注册成功、账号已存在等)---> Map
空值判断:账号,密码,邮箱
验证:(账号已存在、邮箱已存在)——> userMapper.selectByName
注册用户:把用户插入库中
user.setSalt
user.setPassword() 将salt+原密码并加密覆盖原密码
user.setType/setStatus/setActivationCode/setHeader
随机头像设置
user.setHeaderUrl(String.format("http:\\image.nowcoder.com/head/%dt.png",new Random().nextInt(1000)));给用户发激活邮件
Context context = new Context();//Thymeleaf自带对象:模板生成html格式邮件 context.setVariable("email",user.getEmail()); //动态拼接用户能点的路径(每个用户的激活页面是不同的) :101 是用户id,code是激活码 //http://localhost:8080/community/activation/101/code String url = domain+contextPath+"/activation/"+user.getId()+"/"+user.getActivationCode(); context.setVariable("url",url); //生成模板引擎 String content = templateEngine.process("/mail/activation",context); mailClient.sendMail(user.getEmail(), "账号激活",content);
//userService
public Map<String,Object> register(User user){
Map<String ,Object> map = new HashMap<>();
//先对空值做判断
if(user==null){
throw new IllegalArgumentException("参数不为空");
}
if(StringUtils.isBlank(user.getUserName())){
map.put("usernameMsg","账号不能为空!");
return map;
}
if(StringUtils.isBlank(user.getPassword())){
map.put("passwordMsg","密码不能为空!");
return map;
}
if(StringUtils.isBlank(user.getEmail())){
map.put("emailMsg","邮箱不能为空!");
return map;
}
//账号是否存在
User u = userMapper.selectByName(user.getUserName());
if(u!=null){
map.put("usernameMsg","账号已存在!");
return map;
}
//邮箱验证
u = userMapper.selectByEmail(user.getEmail());
if(u!=null){
map.put("emailMsg","该邮箱已被注册!");
return map;
}
//可以注册了
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5(user.getPassword()+user.getSalt()));
user.setType(0);//普通用户
user.setStatus(0);//未激活
user.setActivationCode(CommunityUtil.generateUUID());//生成一个激活码
//为用户设置随机头像,牛客网的头像库有0-1000 号头像
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png",new Random().nextInt(1000)));
user.setCreateTime(new Date());
userMapper.insertUser(user);
//给用户发邮件,用于激活 模板 activation.html
Context context = new Context();
context.setVariable("email",user.getEmail());
//动态拼接用户能点的路径(每个用户的激活页面是不同的) :101 是用户id,code是激活码
//http://localhost:8080/community/activation/101/code
String url = domain+contextPath+"/activation/"+user.getId()+"/"+user.getActivationCode();
context.setVariable("url",url);
//生成模板引擎
String content = templateEngine.process("/mail/activation",context);
mailClient.sendMail(user.getEmail(), "账号激活",content);
return map;
}
7.控制器
将userService注入
定义方法处理用户的注册请求:register页面 post
@RequestMapping(path = "/register",method = RequestMethod.POST) public String register(Model model, User user)Map<String,Object> map = userService.register(user); if(map==null||map.isEmpty()){ model.addAttribute("msg","注册成功,我们已向你的邮箱发送了激活邮件,请尽快激活!"); model.addAttribute("target","/index"); return "/site/operate-result";//跳转到跳转页面 }else{//携带信息,重新回到注册页面,把service层的三个信息都发回,如果是空的就不显示 model.addAttribute("usernameMsg",map.get("usernameMag")); model.addAttribute("passwordMsg",map.get("passwordMag")); model.addAttribute("emailMsg",map.get("emailMag")); model.addAttribute("user",user);//测试 return "/site/register";
8.激活账号
在service层添加业务
public int activation(int userId,String code){//传入用户id,和激活码code,查询激活码 User user = userMapper.selectById(userId); if(user.getStatus()==1){//已经激活了 return ACTIVATION_REPEAT; }else if(user.getActivationCode().equals(code)){ userMapper.updateStatus(userId,1);//修改激活状态 return ACTIVATION_SUCCESS; }else { return ACTIVATION_FAILURE; } }Controller
@RequestMapping(path = "/activation/{userId}/{code}",method = RequestMethod.GET) public String activation(Model model, @PathVariable("userId") int userId,@PathVariable("code") String code){ int result = userService.activation(userId,code); if(result==ACTIVATION_SUCCESS){ model.addAttribute("msg","激活成功!"); model.addAttribute("target","/login"); }else if(result==ACTIVATION_REPEAT){ model.addAttribute("msg","该账号已经激活过!"); model.addAttribute("target","/index"); }else{ model.addAttribute("msg","激活失败!"); model.addAttribute("target","/index"); } return "/site/operate-result"; }
请求:必须是POST请求, 表单: enctype= "multipart/form-data Spring MVC:通过MultipartFile处理.上传文件
如果有人知道一些路径,就可以在没登入时进入某些页面,应该配置拦截器阻止非法访问。
●使用拦截器
-在方法前标注自定义注解
拦截所有请求,只处理带有该注解的方法
●自定义注解
常用的元注解:
@Target、@Retention、@Document、 @Inherited
如何读取注解:
Method . getDeclaredAnnotations ()
Method . getAnnotation (Class<T> annotationClass)
1.先定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPoliy.RUNTIME)
public @interface LOginRequired{
//不用写东西,只是标记的作用
}
2.在需要的方法前加上该注解
3.定义拦截器
@Autowired
public HostHolder hostHolder;//尝试获取当前的用户来判断登入
@Component
public Class LoginRequiredIntercepter implements HandlerInceptor{
@override
public boolean prehander(HttpServletRequest request,HttpServlet response, Object handler) throws Exception{
//参数的 Object handler是拦截的目标,目标是方法才能执行
if(handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
//通过它的方法来直接获取拦截到的方法
sMethod method = handlerMethod.getMethod();
//有了方法对象,尝试从方法对象中获取注解
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if(loginRequired != null && hostHolder.getUser()== null){//且获取不到登入用户
response.sendRedirect(request.getContextPath()+"/login");
return false;
}
}
}
}
4.将拦截器注入WebMvcConfig
//1.先注入
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
//2.然后在addInterceptor加入
registry.addInterceptor().excludePathPatterns()
前缀树,建树,初始化postConstruct,字符匹配
1.在resource 下创建文件,写入敏感词
2.在util包下创建 sensitiveFilter
//@component
class sensitiveFilter{
private class TrieNode{
//结束标识
private boolean isEnd = false;
//子结点
private Map<Character,TrieNode> subNodes = new HashMap<>();
//getter setter方法
//...
//添加子节点
public void addSubNode(Character c,TrieNode node){
subNodes.put(c,node);
}
public TrieNode getSubNode(Character c){
return subNodes.get(c);
}
}
//本类的其余内容
//Logger.........
//敏感词所替换的常量 ***
//根节点:只要首次访问时就有就行了
public TrieNode rootNode = new TrieNode();
//初始化方法
@PostConstruct
public void init(){
//try的括号里执行下两步
//想获取数,把txt文件的读取出来,我们获取类加载器,从类路径下加载资源(idea中项目的target包下的classes包)
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");//注意加上try-catch-finaly
//转化成缓冲流效率高
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
//try的内部
String keyword;
while((keyword=reader.readLine())!=null){
//添加到前缀树中
this.addKeyword(keyword);
}
}
//addKeyword(String word) 方法
private void addKeyword(String keyword){
TrieNode tempNode = rootNode;
for(int i = 0;i<keyword.length();i++){
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);//有的话就获取,没有的话就新建
if(subNode==null){
//初始化子节点
subNode = new TrieNode();
tempNode.addSubNode(c,subNode);
}
//指向子节点,进入下一层循环
tempNode = subNode;
//最后一个字符
if(i==keyword.length()-1){
tempNode.setKeyWordEnd(true);
}
}
}
//检索的方法 ,返回一个已经替换过的字符串
public String filter(String text){
if(StringUtils.isBlank(text)){return null};
//根据图解,需要三个指针
TrieNode tempNode = rootNode;
int begin = 0;
int position = 0;
//存结果
StringBuilder sb = new StringBuilder();
while(position<text.length()){
char c= text.charAt(position);
//跳过符号逻辑:比如 ♥杀♥人♥ :封装个方法
if(isSymbol(c)){
//如果当前指针指向根节点,直接输出
if(tempNode==rootNode){
sb.append(c);
begin++;
}
//无论符号在开头或者中间,position都向下走一步
position++;
contiune;
}
//检查下个节点
tempNode = tempNode.getSubNode(c);
if(tempNode==null){
//以begin开头的字符串不是敏感词,则将begin这个字符记录
sb.append(text.charAt(begin));
//进入下一位置
position = ++begin;
//重新指向根节点
tempNode = rootNode;
}else if(tempNode.isKeywordEnd){
//发现了敏感字,以begin开头,position结尾
sb.append(REPLACEMENT);
begin = ++position;
tempNode = rootNode;
}else {
position++;
}
}
//补充,循环结束了,但是最后begin到posotion的字符还没记录进去
sb.append(text.substring(being));
}
//判断是否为符号
private boolean isSymbol(Character c){
//c<0x2E80||c>0X9FFF 是东亚文字范围
return !CharUtils.isAsciiAlphanumer(c)&&(c<0x2E80||c>0X9FFF);
}
}
通过前缀树来实现,用双指针指向输入的字符串,一个一个字遍历,当检查到有敏感词时就替换,利用工具类来跳过特殊符号
示例:Jquery发送ajax请求:
Service层--发布帖子+敏感词过滤
Controller层
传入帖子id
service层
Controller层:使用到Rustful风格,需要将discussPostId传入,使用 @PathVariable
过程:调用业务层,将帖子信息查询,将得到结果给Model ,通过addAttribute
需要将查到的userId转化为用户信息,调用UserService来获取
(以后开发)帖子的回复
第一类丢失更新:
某一个事务的回滚,导致另一个已提交的数据丢失了。
第二类丢失更新:
某一个事务提交,导致另一个已更新的数据丢失了。
不可重复读:同一事务,两次读取的数据不一致 幻读:同一事务内,同一个表两次查询的行数不一致
声明式事务管理:
需要加上注解表示是事务
还有一个参数是事务传播机制:表示调用了另一个事务
我们新建一个测试类:(前面先加上注解)
编程式事务:
1.首先定义实体entity,与数据库中属性对应
2.数据访问层:
3.Mapper
4.新增业务组件
查询帖子的业务在帖子详情Discusspost业务上
在业务层:添加评论,再更新评论数量,是两个DML操作,需要使用到事务管理
mapper:
由于新增了评论,则需要再DiscussPost业务中新增 “更新评论数量”,使得查看帖子就能查看评论
在业务层添加:
本小节重点:增加评论,在service层新增 由于这其中包含两个DML,因此采用事务管理
Controller:
由spring 提供的注解:
将错误页面添加到template目录下:由springBoot统一处理
为了完善异常处理和通知:完成以下配置
在Controller新建包advice,新建类 ExceptionAdvice,使用注解 @ControllerAdvice
,这样组件会扫描所有的bean,因此限定其扫描带有Controller注解的bean
方法:
@ControllerAdvice(annotation = Controller.class)
public class ExceptionAdvice{
//先将日志组件注入
private static final Logger logger = LoggerFactory.getLogget(EcxeptionAdvice.class);
//处理异常的方法需要这个注解
//方法必须是 public void 的,参数是Exception,和req,resp
@ExceptionHandler(annotation = Controller.class)
public void handler(Exception e,HttpServletRequest request,HttpServletResponse response) throws IOException{
//将异常计入日志
logger.error("服务器发生异常:"+e.getMessage());
//我们想更详细的异常信息:栈信息:遍历栈
//栈中元素是 StackTraceElement
for(StackTraceElement elemnt :e.getStackTrace()){
logger.error(element.tostring());
}
//由于这里是重定向 500这个页面,只适用于同步的请求
//如果是异步请求,需要返回json/XML
//通过request来判断
String xRequestedWith = request.getHeader("x-requested-with");
if("XMLHttpRequest"==xRequestedWith){//发现是异步请求
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();//这里需要抛出异常
writer.write(CommunityUtil.getJSONString(1,"服务器异常"));
}else{//是普通请求的话,就重定向
response.sendRedirect(request.getContextPath()+"/error");
}
}
}
//使用该方法的好处是,不需要在任何Controller上改动就能统一处理问题
采用ControllerAdvice注解,创建一个异常处理类,参数里只要筛选Controller层的类,然后实现方法handler,采用ExceptionHandler注解,方法参数传递异常e,response和request,具体处理流程:使用日志组件写入日志,打印栈,重定向response.sendRedirect
到500页面.
传统方法:将记录日志的组件封装到方法里,然后在需要的地方调用
但这个是系统需求,在业务方法里面耦合的话很不好,将来要改动的话很难。
采用AOP
@Component
@Aspect
public class ServiceLogAspect{
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect);
//首先申明切点
//加上 PointCut注解,声明织入的位置
@PointCut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointCut(){
}
//使用前置通知
@Before("pointCut()")
public void before(JointPoint jointPoint){
//格式 用户[ip] 在 XXX时间访问了 XXX方法
//这里需要获取用户地址,但不要在这声明Request对象,而使用工具类RequestContextHolder
ServletReqeustAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteHost();
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
//最后访问某个类的方法,需要连接点参数jointPoint
String target = jointPoint.getSignature().getDeclaringTypeName()+"."+jointPoint.getSignature().getName();
logger.info(String.format("用户[%s],在[%s]访问了[%s].",ip,now,target));
}
}
快照形式:RDB:整体存入硬盘中
日志形式:AOF:将日志记入硬盘中,实时性好,但维护耗时
在github下载windows的redis,安装后自动运行,其默认端口号为:6379.将其配置在系统变量
redis-cli #启动客户端
#系统默认16个库,采用0-15命名
>>select 1 #切换到库1
OK
>>flushdb #刷新内容
#String 类型
>>set test:count 1
OK
>>get test:count
"1"
>>incr test:count #变量自增
(integer) 2
>>decr test:count
(integer) 1
# Hash 类型数据,值也是键值对 hset KEY FIELD VALUE
>>hset test id 1
(integer) 1
>>hset test username zhangshan
"zhanshan"
>>hget test id
"1"
# List 列表:支持左、右插入 左、右取值
>>lpush test 101 102 103 #相当于将101,102,103左插入列表,此时为[103, 102, 101]
(integer) 3
>>llen test
3
>>lindex test 0
103
>>lrange test 0 2 #表示范围从0到2
1) "103"
2) "102"
3) "101"
>>rpop test #从右侧弹出
"101"
#Set 集合
>>sadd test aaa bbb ccc ddd eee
(integer) 5
>>sard test # 统计多少元素
(integer) 5
>>spop test #随机弹出一个元素
"ccc"
>>smembers test # 集合剩余元素
1) "aaa"
2) "bbb"
3) "ddd"
4) "eee"
#socket set有序集合 :给定分数,按分数排序
>>zadd test 10 aaa 20 bbb 30 ccc 40 ddd 50 eee
(integer) 5
>>zcard test # 统计多少元素
(integer) 5
>>zscore test ccc #查询某个值的分数
"30"
>>zrank test ccc # 返回某个值的排名
(integer) 2
>>zrange test 0 2
"aaa"
"bbb"
"ccc"
#全局命令
>>keys * #查询库中有多少
>>keys t* #以 t 开头的有多少个
>>type test # 查询是什么数据类型的
>>exists test #查询是否存在
>> del
>>expire test 60 #设置该keys的过期时间为60s,用于验证码
Springboot将Redis的键值对的键由String转成Object,但我们常用还是String,因此要重新配置
首先在application.properties配置
spring.redis.database=11 #随便选一个库就行
spring.redis.host=localhost
spring.redis.port=6379
编写配置类:
@Configuration
public class RedisConfig{
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
//要能访问数据库,需要创建连接:注入工厂(在形参中注入)
//在方法中实例化Bean
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//主要配的是序列化的方式(将java数据存入redis)
//1.设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
//2.设置value的序列化方式
template.setValueSerializer(RedisSerilalizer.json());
//特殊:设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());
//特殊:设置hash的value序列化方式
template.setHashValueSerialize(RedisSerializer.json());
//使设置的配置生效
template.afterPropertiesSet();
return template;
}
}
//测试
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testStrings(){
String redisKey = "test:count";
redisTemplate.opsForValue().set(redisKey,1);
sout(redisTemplate.opsForValue().get(redisKey));
sout(redisTemplate.opsForValue().increment(redisKey));
}
测试Hash同理
测试列表
测试集合
对Keys的测试
编程式事务
由multi()
开启事务,由exec()
提交事务,在事务之间的操作保存在队列里并不会执行,因此查询语句不会显示查到结果
由于redis操作简单,因此不开发mapper,而直接开发service层,面向key 实现
//先写个工具类
public class RedisKeyUtil{
private static final String SPLIT = ":";//存下分隔符
//我们将帖子和帖子的评论称为实体
private static final String PREFIX_ENTITY_LIKE = "like:entity";
//返回某个实体的赞,拼成以下形式://将点过赞的用户id存入集合中
// like:eneity:{entityType}:{entityId} -> set(userId)
public static String getEntityLikeKey(int entityType,int entityId){
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}
}
// LikeService
@Service
public class LikeService{
@Autowired
private RedisTemplate redisTemplate;
//点赞
public void like(int userId,int entityType,int entityId){
//存入Redis的key统一命名
String entityLikeKey = RedisKeyType.getEntityLikeKey(entityType,entityId);
//检查是否存在(点过赞再次点击就是取消)
boolean isMember = redisTemplate.opsForSet().idMemeber(entityLikeKey,userId);
if(isMember){
redisTemplate.opsForSet().remove(entityLikeKey,useId);
}else{//否则就添加数据
redisTemplate.opsForSet().add(entityLikeKey,useId);
}
}
//查询实体的点赞数量
public long findEntityLikeCount(int entityType,int entityId){
String entityLikeKey = RedisKeyType.getEntityLikeKey(entityType,entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
//查询某人对某实体的点赞状态,返回整数(以后业务扩展出点踩)
public int findEntityLikeStatus(int userId,int entityType,int entityId){
String entityLikeKey = RedisKeyType.getEntityLikeKey(entityType,entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey,userId)?1:0;
}
}
//表现层:点赞是异步请求,即时刷新
@Controller
public class LikeController{
@Autowired
private LikeService likeService;
@Autowired
private HostHolder hostHolder;
@Requestmapping(path = "/like",method =RequestMethod.POST)
@ResponseBody//异步请求
public String liek(int eneityType,int entityId){
User user = hostHolder.getUser();
//以后会使用Spring Security对拦截器重构
//点赞实现
likeService.like(user.getId(),entityType,entityId);
//数量
long likeCount = likeService.findEntityLikeCount(entityType,entityId);
//状态
int likeStatus = likeService.findEntityLikeStatus(user.getID(),entityType,entityId);
//用map 封装
Map<String,Object> map = new HashMap<>();
map.put("likeCount",likeCount);
map.put("likeStatus",likeStatus);
//最终返回json格式数据
return CommunityUtil.getJsonString(0,null,map);//这是之前封装的工具
}
}
修改模板
找到点赞的位置,修改href为空,新增 onclick
标签,里面调用js函数like(this,1,${post,id})
this 是从本页面三种类型的赞找到,1是表示帖子的entityType
写个js文件
btn是当前按钮,获取按钮下的标签b,和i,修改其值
运行后可以使用,但是初始显示的赞数量不对
高吞吐量,消息持久化:对硬盘的顺序读取效率是高于内存的随机读取的。 高可靠性:是分布式的 Broker:kafka集群上每一个服务器称为Broker Zookeeper:用于管理集群 Topic:用于点对多生产消费方式,消费者发布后的, 用于存放消息的位置 Partition:对topic位置的分区 offset:消息在分区内存放的索引 Replica:副本,主副本和从副本(只做备份不做响应),主副本挂掉,会从众多从副本重新选
下载kafka,做初始配置。(.sh 是linux命令,.bat 是Windows命令)默认端口是9092
1.配置zookeeper,将原本liunx地址改成windows下地址
2.配置server.propertise: 默认日志地址改为windows
启动测试:需要先启动zookeeper,并指定配置文件
再打开另一个命令行,启动kafka
--create
创建主题
--bootstrap--server
在哪个主题上
--replication-factor 1
创建 1 个副本
--partitions 1
一个分区
--topic
主题名字
接下来发送消息,以生产者身份调用
--broker-list
选择服务器(现在只有一个) --topic test
选择主题。完成后下行出现三角
发送了消息,现在是阻塞状态,然后我们再打开一个cmd启用消费者
然后在生产者输入,消费者会自动出现(有点像聊天)
spring中整合的主要依赖kafkaTemplate
(用的时候直接注入)
1.mvn引入kafka
2.配置properties
先打开kafak文件的consumer.properties
写测试
public class KafkaTests{
@Autowired
private KafkaProducer kafkaProducer;
@Test
public void testKafka(){
kafkaProducer.sendMessage("test","你好");
kafkaProducer.sendMessage("test","在吗");
//等待一下让消费者输出
try{
Thread.sleep(10000);
}catch(Exception e){
e.printStackTrace();
}
}
}
//在这里写生产者和消费者
@Component
class KafkaProducer{
@Autowired
private KafkaTemplate kafkaTemplate;
public void sendMessage(String topic,String content){//参数是主题和内容
kafkaTemplate.send(topic,content);
}
}
@Component
class KafkaConsumer{
//不需要kafkaTemplate,因为是被动的接受参数
@KafkaListener(topics={"test"})//spring自动监听这些主题,阻塞监听,然后交给方法
public void handleMessage(ConsumerRecord record){
sout(record);
}
}
关键点:生产者是主动调用的,而消费者被动的
以事件作为驱动,定义事件class
public class Event{
private String topic;
private int userId;
private int entityType;
private int entityId;
private int entityUserId;
private Map<String,Object> data = new HashMap<>();//存其他还不知道的东西
//对应getter,setter
//修改setter方法,返回值Event,可以每次增加一个属性,返回再次增加
//修改setData方法,使之不要直接传map,
public Event setData(String key,Object value){
this.data.put(key,value);
return this;
}
}
接着开发事件的生产者和消费者:
新建一个包Event
//生产者需要调用kafkaTemplate
public class EventProducer{
@Autowired
private KafkaTemplate kafkaTemplate;
//处理事件
//
public void fireEvent(Event event){
//将事件发布到指定的主题
//内容为json格式
kafkaTemplate.send(event.getTopic(),JSONObject.toJSONString(event));
}
}
public class EventConsumer{
private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
//处理事件是为了给message表插入数据
@Autowired
private MessageService messageService;
//可以一个方法消费一个主题,一个方法消费多个主题
@KafkaListener(topics={/*写在接口的常量*/TOPIC_COMMENT,TOPIC_LIKE,TOPIC_FOLLOW})
public void handlerCommentMessage(ConsumerRecord record){
if(record==null||record.value()==null){
logger.error("消息内容为空");return ;
}
//将JSON格式字符串恢复成对象
Event event= JSONObject.parseObject(record.value().toString(),Event.class);
if(event==null){
logger.error("消息格式错误");return;
}
Message message = new Message();
message.setFromId(1);//或者存入接口常量
message.setToID(event.getEntityUserId());
message.setConversationId(event.getTopic());
message.setCreateTime(new Date());
//我们需要在通知中拼出语句,谁 干了什么,然后链接到指定位置
Map<String,Object> content = new HashMap<>();
content.put("userId",event.getUserID());//获取哪个用户干了什么
content.put("entityType",event.getEntityEype());
content.put("entityId",event.getEntityId());
if(!event.getData().isEmpty()){
for(Map.Entry<String,Object> entry:event.getData().entrySet()){
content.put(entry.getKey(),entry.getValue());
}
}
message.setContent(JSONObject.toJSONSTRING(content));
messageService.add(message);
//方法是消费三个主题的数据,消费的逻辑是发送一条消息,消息构造一样
}
}
//CommentController 添加
//注入事件
@Autowired
private EventProducer eventProducer;//在LikeController和FollowController也要加上
//在addComment函数中添加代码
{
//在commentService.addComment(comment); 之后操作
//触发评论事件
//1.构造事件对象,将内容包含进来
Event event = new Event().setTopic(1/*引用常量*/).setUserId(hostHolder.getUser().getId())
.setEntityType(comment.getEntityType())
.setEntityId(comment.getEntityId())
.setData("PostId",disscussPostId);//用于链接时 需要帖子ID,这里存进map
//区分帖子还是评论来获得对应UserId
if(commetn.getEntityType()==?){
DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
}else{
//..
}
//之后我们再调用Producer处理事件
eventProducer.fireEvent(event);//新线程执行,不会影响后续业务
}
将上一节存入数据库的通知显示在页面上
dao层
//MessageMapper添加:
//查询某个主题下最新通知 查询某个主题下通知数量 未读的通知数量
Message selectLatestNotice(int userId,String topic);
int selectNoticeCount(int userId,String topic);
int selectNoticeUnreadCount(int userId,String topic);
mapper
<!--Message-mapper-->
<select id="selectLatestNotice" resultType="Message">
select <include refid="selectFields"></include>
from message
where id in(
select max(id) from message
where status!=2 and from_id=1' and to_id =#{userId} and conversation_id =#{topic}
)
</select>
<select id="selectNoticeCount" resultType="int">
select count(id)
from message
where status!=2
and from_id =1
and to_id = #{userId}
and conversation_id = #{topic}
</select>
<select id="selectNoticeUnreadCount" resultType="int">
select count(id)
from message
where status=0<!--表示未读-->
and from_id =1
and to_id = #{userId}
<if test="topic!=null">
and conversation_id=#{topic}
</if>
</select>
service
//MessageService
学习笔记:(13条消息) Elasticsearch学习笔记_巨輪的博客-CSDN博客_elasticsearch学习笔记
Restful:每个URI代表一个资源;客户端通过四个http动词,客户端不能操作资源而是提交请求;客户端和服务器交互通过表现层。
ES中的索引与数据库的database对应()(6.0后 索引对应table)
ES中的类型与数据库的 table对应(逐渐废弃)
文档:一条数据,一行,文档数据结构一般为json,json的一个属性为字段
集群的每一个服务器称为节点
分片:对索引的进一步划分(并发地存)
副本:对分片的备份
下载修改配置 elasticsearch.yml
cluster.name: nowcoder #集群名字
path.data: d:/work/data/elasticsearch-6.4.3/data #数据目录
path.log: d:/work/data/elasticsearch-6.4.3/logs #日志
然后配置环境变量
然后安装中文分词插件:在github上搜索 elasticsearch ik
必须解压到 work/elasticsearch-6.4.3/plugins/ik
目录下
如何启动:bin /elasticsearch.bat
提示绑定了9200端口,表示成功
打开命令行访问:
curl -X GET "localhost:9200/-cat/health?v" #查看集群健康状况 v表示显示标题
查看集群有什么节点
curl -X GET "localhost:9200/-cat/nodes?v"
查看当前服务器有多少个索引?(ES6.0以上索引就是一个表)
curl -X GET "localhost:9200/-cat/indices?v"
没有数据,及没有索引
如何建立索引?返回json格式
curl -X PUT "localhost:9200/test" #使用put
然后再次查询有多少索引
索引的健康状况为yellow,然后删除索引:
采用postman来操作:添加一条字段(test索引下,id 为 1)
再查询这条数据:
在表中查询,使用search,(不加其他参数则都会命中)
查询content
会对关键字提供分词
如果要复合查询:(在put请求中使用row格式body,采用json格式)
关键字:query:“互联网” 在哪些字段:fields:标题和内容都查询
ElasticsearchTemplate是类,ElasticsearchRepository是接口
ES的端口有9200(基于Http),9300(基于TCP)
ES的底层基于netty,而redis底层也基于netty,会出现冲突, 在nettyRuntime类中有个setAvailableProcessors 方法,redis调用了则当ES 的Netty4Utils也调用就会出错,需要设置参数
spring.data.elasticsearch.cluster-name=nowcoder#安装时在配置文件修改的
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
接下来解决一个冲突
//在CommunityApplication中,添加一个@PostContrust 修饰的方法
@PostConstruct
public void init(){
//解决netty启动冲突问题
System.setProperty("es.set.netty.runtime.available.processors","false");
}
//下面是main方法
我们需要通过注解来修改之前的实体类
//通过注解让ES与实体类属性映射
@Document(indexName="discusspost",type="_doc",shards=6,replicas=3)//创建几个分片和副本
public class DiscussPost{
@ID
private int id;
@Field(type=FieldType.Integer)//其他属性
private int userId;
//两个分词器,第一个是存储,第二个是搜索,存储时采用尽可能多的分词
@Field(type=FieldType.Text,analyzer="ik_max_word",searchAnalyzer = "ik_smart")//文本类型,存储时解析器和搜索时解析器
private String title;//搜索的关键字段
//....其余类似
}
在service包下创建elasticsearch包,创建名为 DiscussPostRepository
接口,加上@Repository
@Repository
//接口需要继承ElasticsearchRepository,并指定泛型存的实体类和主键
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost,Integer>{
}
//测试
//由于是需要从mysql中取出,转存到es中
{
@Autowired
public DiscussPostMapper = discussMapper;
@Autowired
private DiscussPostReposiory = discussPostReposiory;
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;//有时候Reposiory接口处理不了的需要使用Template子类来解决
//向ES服务处插入数据
@Test
public void testInsert(){
discussRepository.save(discussMapper.selectDiscussPostById(241));
}
//查询
@Test
public void testSearchByRepository(){
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(QueryBuilders.multiMatchQuery("互联网寒冬","title","content"))
.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("creatTime").order(SortOrder.DESC))
.withPageable(PageRequest.of(0,10))
.withHighlightFields(
new HighlightBuilder.Fields("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Fields("content").preTags("<em>").postTags("</em>"),
).build();
//通过Page对象对查询结果封装
Page<DiscussPost> page = discussRepository.search(searchQuery);
sout(page.getTotalElements());
sout(page.getTotalPages());
sout(page.getNumber());
//由于Repository接口底层实现,并没有将高亮结果整合,下面使用Template查询
}
@Test
public void testSearchByTempalte(){
//一样的SearchQuery
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(QueryBuilders.multiMatchQuery("互联网寒冬","title","content"))
.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("creatTime").order(SortOrder.DESC))
.withPageable(PageRequest.of(0,10))
.withHighlightFields(
new HighlightBuilder.Fields("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Fields("content").preTags("<em>").postTags("</em>"),
).build();
Page<DiscussPost> page = elasticTemplate.queryForPage(searchQuery,DiscussPost.class,new SearchResultMapper(){
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse,Class<T> aClass,Pageable pageable){
//先通过response取到这次搜索得到的数据
SearchHits hits = response.getHits();
if(hits.getTotalHits()<=0) return null;
//最终需要将数据封装到集合里返回
List<DiscussPost> list = new ArrayList<>();
for(SearchHit hit:hits){
DiscussPost post = new DiscussPost();
//由于是Json格式的,需要toString
String id = hit.getSourceAsMap().get("id").toString();
post.setId(Integer.valueOf(id));
String userId = hit.getSourceAsMap().get("userId").toString();
post.setUserId(Integer.valueOf(userId));
//由于有可能匹配的字段只在title或content中,所以先将原始title,content写入防止空
//原始title,非高亮显示的
String title = hit.getSourceAsMap().get("title").toString();
post.setTitle(title);
String content = hit.getSourceAsMap().get("content").toString();
post.setContent(content);
String status = hit.getSourceAsMap().get("status").toString();
post.setStatus(Integer.valueOf(status));
String createTime = hit.getSourceAsMap().get("userId").toString();
post.setCreateTime(new Date(Long.valueOf(createTime)));//ES在存日期时转成long
String commentCount = hit.getSourceAsMap().get("commentCount").toString();
post.setCommentCount(Integer.valueOf(commentCount));
//获取高亮内容
HeightlightField titleField = hit.getHighlightFields().get("title");
if(titleField!=null){
//getFregments 返回的是数组,因为一段内容中可能出现多次匹配串
post.setTitle(titleField.getFragments()[0].toString();
}
HeightlightField contentField = hit.getHighlightFields().get("content");
if(contentField!=null){
//getFregments 返回的是数组,因为一段内容中可能出现多次匹配串
post.setContent(contentField.getFragments()[0].toString();
}
list.add(post);
//返回类型是 AggregatedPage<T> 接口,需要构造一个实现类
}
//需要参数:具体查看底层实现
return new AggregatedPageImpl<>(list,pageable,hits.getTtotalHits(),response.getAggregations(),
response.getScrollId(),hits.getMaxScore());
}
});
//相同代码
Page<DiscussPost> page = discussRepository.search(searchQuery);
sout(page.getTotalElements());
sout(page.getTotalPages());
sout(page.getNumber());
}
}
功能:
- 搜索服务 将帖子保存到ES服务器 从ES服务器删除帖子 从ES服务器搜索帖子
- 发布事件 发布帖子时,将帖子异步提交到ES服务器 增加评论时,将帖子异步提交到ES服务器 在消费组件增加方法,消费帖子发布事件
- 显示结果 在控制器处理搜索请求,在HTML上显示结果
先解决问题:在discussPost.xml中的sql语句:
<insert id = "insertDiscussPost" parameterType="DiscussPost"><!--这里没有声明主键是什么-->
<!--mybaits不会把生成的组件存到实体类中-->
</insert>
<!--因此添加:-->
<insert id = "insertDiscussPost" parameterType="DiscussPost" keyProperty="id">
处理业务层:新建elsasticSearchService
@Service
public class ElasticSearchService{
@Autowired
private DiscussPostRepository discussRepository;
@Autowired
private ElasticsearchTemplate elasticTempate;//想要高亮显示就需要
//方法:向ES服务器提交新产生的帖子
public void saveDiscussPost(DiscussPost post){
discussRepository.save(post);
}
//删除
public void deleteDiscussPost(int id){
discussRepository.deleteById(id);
}
//搜索:返回spring提供的page类型
//参数:keyword 关键字, current当前页 limit每页多少数据
public Page<DiscussPost> searchDiscussPost(String keyWord,int current,int limit){
//拷贝测试类中方法,修改
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(QueryBuilders.multiMatchQuery(keyword,"title","content"))
.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("creatTime").order(SortOrder.DESC))
.withPageable(PageRequest.of(current,limit))
.withHighlightFields(
new HighlightBuilder.Fields("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Fields("content").preTags("<em>").postTags("</em>"),
).build();
return elasticTemplate.queryForPage(searchQuery,DiscussPost.class,new SearchResultMapper(){
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse,Class<T> aClass,Pageable pageable){
//先通过response取到这次搜索得到的数据
SearchHits hits = response.getHits();
if(hits.getTotalHits()<=0) return null;
//最终需要将数据封装到集合里返回
List<DiscussPost> list = new ArrayList<>();
for(SearchHit hit:hits){
DiscussPost post = new DiscussPost();
//由于是Json格式的,需要toString
String id = hit.getSourceAsMap().get("id").toString();
post.setId(Integer.valueOf(id));
String userId = hit.getSourceAsMap().get("userId").toString();
post.setUserId(Integer.valueOf(userId));
//由于有可能匹配的字段只在title或content中,所以先将原始title,content写入防止空
//原始title,非高亮显示的
String title = hit.getSourceAsMap().get("title").toString();
post.setTitle(title);
String content = hit.getSourceAsMap().get("content").toString();
post.setContent(content);
String status = hit.getSourceAsMap().get("status").toString();
post.setStatus(Integer.valueOf(status));
String createTime = hit.getSourceAsMap().get("userId").toString();
post.setCreateTime(new Date(Long.valueOf(createTime)));//ES在存日期时转成long
String commentCount = hit.getSourceAsMap().get("commentCount").toString();
post.setCommentCount(Integer.valueOf(commentCount));
//获取高亮内容
HeightlightField titleField = hit.getHighlightFields().get("title");
if(titleField!=null){
//getFregments 返回的是数组,因为一段内容中可能出现多次匹配串
post.setTitle(titleField.getFragments()[0].toString();
}
HeightlightField contentField = hit.getHighlightFields().get("content");
if(contentField!=null){
//getFregments 返回的是数组,因为一段内容中可能出现多次匹配串
post.setContent(contentField.getFragments()[0].toString();
}
list.add(post);
//返回类型是 AggregatedPage<T> 接口,需要构造一个实现类
}
//需要参数:具体查看底层实现
return new AggregatedPageImpl<>(list,pageable,hits.getTtotalHits(),response.getAggregations(),
response.getScrollId(),hits.getMaxScore());
}
});
}
}
表现层:采用事件【异步】方式来同步数据
发布帖子:找到DiscussPostController
//发布帖子已有的方法
public String addDiscussPost(String title,String content){
User user = hostHolder.getUser();
if(User==null){
return CommunityUtil.geetJSONString(403,"你还没有登入哦!");
}
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
//新增位置:触发发帖事件
Event event = new Event().setTopic("publish").setUserId(user.getId()).setEntityType(POST/*常量里设置*/)
.setEntityId(post.getId());
//在前面注入 EventProducer eventProducer
eventProducer.fireEvent(event);
return CommunityUtil.getJSONStrign(0,"发布成功!");
}
发布评论:CommentController
//加到方法里,评论和帖子处理不同
EventConsumer
//新增主题和方法
//消费发帖事件
@KafkaListener(topics={TOPIC_PUBLISH})//常量接口里的"publish"
public void handlePublishMessage(ConsumerRecord record){
if(record==null || record.value()==null){
logger.error("消息队列为空!");return ;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if(event==null){
logger.error("消息格式错误");return ;
}
//从事件消息里得到帖子id,查到帖子,存到ES服务器中
//注入DiscussPostService, ElasticsearchService
DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPsot(post);
}
新增searchController类
//注入 ElasticsearchService , UserService, LikeService
//方法参数: 关键字,分页,视图模型
//由于采用GET方法,传入关键字就没有请求体来传,采用url传入
// /search?keyword=xxx
@RequestMapping(path="\search", method = GET)
public String search(String keyword, Page page, Model model){
//搜索帖子,传入的page从1开始,而方法从0开始
//由于搜索对象和自定义的Page类冲突了,所以带上全类名
org.springframework.data.domain.Page<DiscussPost> searchResult =
elasticsearchService.searchDiscussPost(keyword, page.getCurrent()-1, page.getLimit());
//聚合数据:还包含用户信息
List<Map<String,Object>> discussPosts = new ArrayList<>();
if(searchResult !=null){
for(DiscussPost post : searchResult){
Map<String,Object> map = new HashMap<>();
map.put("post",post);
map.put("user",userService.findUserById(post.getUserId()));
map.put("likeCount",LikeService.findEntityLikeCount(ENTITY_TYPE_POST,post.getId()));
discussPost.add(map);
}
}
model.addAttribute("discussPost",discussPost);
model.addAttribute("keyword",keyword);
//分页信息
page.setPath("/search?keyword="+keyword);
//多少数据,便于计算总页数
page.setRow(searchResult==null?0:(int)searchResult.getTotalElements());
return "/site/search";
}
HTML
在index.html的header,所有页面的都复用
由于是个input文本框,其内容需要提交到后台,添加 name 属性 name="keyword"
我们在搜索后跳转到页面,该页面的搜索框得保持原输入关键字 th:value="${keyword}"
处理 search.html
Filter和DispatcherServlet都是符合JavaEE规范的。Filter并不在MVC中
在maven中引入spring security后,将自动接管系统,并生成初始登入账号和密码
账号:user
密码://在控制台中生成
进一步处理:用户管理:
对User实体类实现接口 UserDetails,重写以下方法
获取到user对象后:调用以下方法获取其认证,可以得知是管理员还是普通用户
Service层:UserService 新实现接口:UserDetailsService
重写方法:根据用户名查用户
接着实现security对系统的掌控:基于Filter,不需要改写大量的类,而新增securoty配置类
//Config 包下
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private UserService userService;
//重写三个方法 :Configure
@Override
public void configure(WebSecurity web) throws Exception{
web.ignoring().antMatchers("/resources/**");//忽略静态资源的访问
}
/*
一些组件:
AuthenticationManager:认证核心接口
AuthenticationManagerBuilder: 构建AuthenticationManager的工具类
ProviderManager: AuthenticationManager接口的默认实现类
AuthenticationProvider: ProviderManager持有一组AuthenticationProvider,每个AuthenticationProvider负责一种认证
这样ProviderManager就能兼容多种认证,账号密码,微信等等。。
委托模式
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception{
//认证+授权处理
// auth.userDetailsService(userService).passwordEncoder(new pbkd2PasswirdEncoder("123456")); //加密工具和salt
//上方法和之前的不匹配,因为每个用户都可能是不一样的salt
//自定义认证规则
//..截图
}
// Authentication: 用于封装认证信息的接口,不同实现类代表不同认证方式
}
独立性:多个测试的方法不依赖,能随时执行。因此有了以下步骤:
- 初始化数据:为本次测试单独的数据,最终清除数据,因此该测试方法是独立的,不依赖于其他方法的数据
- SpringBoot为了避免写重复的代码,保证初始化的数据可重复使用;按步骤进行
@BeforeClass:在此类初始化之前执行的方法
@AfterClass:在类销毁后执行
@Before:在调用任何测试方法之气,该方法执行一次
@After:
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class SpringBootTests {
//验证测试类的相关注解 @BeforeClass
//由于是在类初始化之前执行的,因此是静态方法
@BeforeClass
public static void beforeClass(){
System.out.println("beforeClass");
}
@AfterClass
public static void afterClass(){
System.out.println("afterClass");
}
@Before
public void before(){
System.out.println("before");
}
@After
public void after(){
System.out.println("after");
}
@Test
public void test1(){
System.out.println("test1");
}
}
beforeClass
before
test1
after
afterClass
Process finished with exit code 0
这样在before中定义数据(注入mapper插入数据)
然后在这个测试类中写多个test方法,统一运行该类,成功就标绿。如何验证成功?采用断言
Assert.assertNotNull(post);
Assert.assertEquals(data.getTitle(),post.getTitle());//判断两数据是否相等
正向代理:代理浏览器 反向代理:代理服务器
putty:用于访问服务器
springboot :起步依赖,端点控制,
本实验用到 spring AMQP 作消息队列
当声明了两个Bean且都是同一父类的子类时,这时getBean获取父类实现类就会报错applicationContext.getBean(father.class)
:
在优先选择的Bean上加上注解 @Primary
但是如果偏要获取之前的Bean,我们在注解@Repository
上加括号写上名字 @Repository("son2")
然后通过名字获取applicationContext.getBean("son2",father.class)
Spring容器还能管理bean的初始化和销毁
@PostConstruct
表示在初始化之后运行方法
@PreDestroy
在销毁后自动运行方法
如果不希望Bean是单例的,加上注解 @Scope("prototype")
,每次访问bean都会返回一个新的
将每一个功能作为一次请求:
请求流程:Controller-> Service-> Dao->DB
社区首页:查询帖子列表
首先开发DAO,先把实体类做好
//DisscussPost
private int id;
private int userId;
private String title;
private String content;
private int type;
private int status;
private Date createTime;
private int commentCount;
private double score;//生成gettersetter
//DiscussPostMapper
public interface DiscussPostMapper {
/**
*
* @param userId 在首页查询不需要id,而以后的个人主页查询需要id,因此开发成动态sql
* @param offset 起始行行号
* @param limit 每页最多显示的数据条数
* @return
*/
List<DiscussPost> selectDiscussPosts(int userId,int offset, int limit);
int selectDiscussPostRows(@Param("userId") int userId);//如果需要动态拼条件,且方法有且只有一个条件,则这个参数前必须起别名
}
<sql id="selectFields">
id,user_id,title,content,`type`,status,create_time,comment_count,score
</sql>
<!--List<DiscussPost> selectDiscussPosts(int userId,int offset, int limit);-->
<select id="selectDiscussPosts" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where status !=2 <!--2 表示拉黑的-->
<if test="userId!=0">
and user_id = #{userId}
</if>
order by type desc, create_time desc
limit #{offset},#{limit}
</select>
<!--int selectDiscussPostRows(@Param("userId") int userId);-->
<select id="selectDiscussPostRows" resultType="int">
select count(id)
from discuss_post
where status !=2
<if test="userId!=0">
and user_id = #{userId}
</if>
</select>
写完后测试一下
@Autowired
private DiscussPostMapper discussPostMapper;
@Test
public void testSelectPosts(){
List<DiscussPost> list = discussPostMapper.selectDiscussPosts(149,0,10);
for(DiscussPost post: list){
System.out.println(post);
}
int rows = discussPostMapper.selectDiscussPostRows(149);
System.out.println(rows);
}
service
@Autowired
private DiscussPostMapper discussPostMapper;
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit){
return discussPostMapper.selectDiscussPosts(userId,offset,limit);
}
public int findDiscussPostRows(int userId){
return discussPostMapper.selectDiscussPostRows(userId);
}
//我们查询到的DiscussPost结果的userI是外键,在页面上需要显示用户名称
//方法一: 同时做关联查询,查询username
//方法二: 针对每一个DiscussPost单独查user,然后组合在一起---当使用Redis更方便
新建UserService
public User findUserById(int id){
return userMapper.selectById(id);
}
视图层:访问首页
@ReqeustMapping(path="/index",method = GET)
public String getIndexPage(Model model){
//我们要获取前十条帖子
List<DiscussPost> list = disscussPostService.finfDiscussPost(0,0,10);
//我们还要查到userId,因此将两个数据组装在map中
List<Map<String,Object>> disscussPost = new ArrayList<>();
if(list!=null){
for(DiscussPost post:list){
Map<String,Object> map = new HashMap<>();
map.put("post",post);
//然后根据User的方法查询用户
User user = userService.findUserById(post.getUserId());
map.put("user",user);
discussPost.add(map);
}
}
//之后将这个List加进Model中
model.addAttribute("discussPosts",discussPosts);
return "/index"
}
然后修改index.html
<!-- 帖子列表 -->
<ul class="list-unstyled">
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
<a href="site/profile.html">
<img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width:50px;height:50px;">
</a>
<div class="media-body">
<h6 class="mt-0 mb-3">
<a href="#" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>
<span class="badge badge-secondary bg-primary" th:if="${map.post.type==1}">置顶</span>
<span class="badge badge-secondary bg-danger" th:if="${map.post.status==1}">精华</span>
</h6>
<div class="text-muted font-size-12">
<u class="mr-3" th:utext="${map.user.userName}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
<ul class="d-inline float-right">
<li class="d-inline ml-2">赞 11</li>
<li class="d-inline ml-2">|</li>
<li class="d-inline ml-2">回帖 7</li>
</ul>
</div>
</div>
</li>
</ul>
这里的 ${map.user.headerUrl} 会标红(但可运行)
其实是调用 map.get("User").getHeaderUrl()
添加page实体,要修改部分gettersetter,作有效判断,并添加方法计算页面总数,起始编号和尾端编号
public class Page {
private int current = 1;//当前页码
private int limit =10;//显示上限
private int rows;//数据总数(用于计算总页数)
private String path;//查询路径
//获取当前页的起始行
public int getOffset(){
return (current-1)*limit;
}
//获取总的叶数
public int getTotal(){
if(rows%limit==0){
return rows/limit;
}else{
return rows/limit+1;
}
}
//从哪开始,从哪结束
public int getFrom(){
int from = current-2;
return Math.max(from, 1);
}
public int getTo(){
int to = current+2;
int total = getTotal();
return Math.min(to, total);
}
public int getCurrent() {
return current;
}
public void setCurrent(int current) {
if(current>=1){
this.current = current;
}
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
if(limit>=1&&limit<=100){
this.limit = limit;
}
}
public int getRows() {
return rows;
}
public void setRows(int rows) {
if(rows>=0){
this.rows = rows;
}
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
这样,要修改Controller,将Page传入
@RequestMapping(path="/index",method = RequestMethod.GET)
public String getIndexPage(Model model, Page page){
//方法调用前,springMVC会自动实例化model和page,且将page注入给model
page.setRows(discussPostService.findDiscussPostRows(0));
page.setPath("/index");
List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
//由于查到的用户id,不是用户名,这里我们把集合遍历,针对每一个DiscussPost,根据userId查询user,将结果组合起来
List<Map<String,Object>> discussPosts = new ArrayList<>();
if(list!=null){
for(DiscussPost post:list){
Map<String,Object> map = new HashMap<>();
map.put("post",post);
User user = userService.findUserById(post.getUserId());
map.put("user",user);
discussPosts.add(map);
}
}
model.addAttribute("discussPosts",discussPosts);
model.addAttribute("page",page);//其实可以不用加
return "/index";
}
修改index.html的分页部分
<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows>0}">
<ul class="pagination justify-content-center">
<li class="page-item">
<a class="page-link" th:href="@{${page.path}(current=1)}">首页</a></li>
<li th:class="|page-item ${page.current==1?'disabled':''}|">
<a class="page-link" th:href="@{${page.path}(current=${page.current-1})}">上一页</a>
</li>
<li th:class="|page-item ${i==page.current?'active':''}|" th:each="i:${#numbers.sequence(page.from,page.to)}">
<a class="page-link" th:href="${i}" th:text="${i}">1</a>
</li>
<li th:class="|page-item ${page.current==page.total?'disabled':''}|">
<a class="page-link" th:href="@{${page.path}(current=${page.current+1})}">下一页</a>
</li>
<li class="page-item">
<a class="page-link" th:href="@{${page.path}(current=${page.total})}">末页</a>
</li>
</ul>
</nav>
响应状态码的含义
200 OK
201 Created 请求成功,并因此创建了一个新的资源
202 Accepted 请求已收到,但还未响应
302 Found 请求的资源现在从不同的URI响应,这是临时的
400 Bad Request 语义有误,无法被服务器理解
403 Forbidden
404 Not Found 资源在服务器上未发现
500 Internal Server Error 服务器遇到了不知如何处理
设置日志
默认日志工具 : logback
接口:Logger
方法:
//不同级别:
trace();
debug();
info();
warn();
error();
创建日志测试类:
//测试类的注解
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration
public class LoggerTests {
//注意是 slf4j 包下的
private static final Logger logger = LoggerFactory.getLogger(LoggerTests.class);
@Test
public void testLogger(){
System.out.println(logger.getName());
logger.debug("debug-log");
logger.info("info-log");
logger.warn("warn-log");
logger.error("error-log");
}
}
在配置中:
# logger
#logging.level.com.nowcoder.community=debug
#logging.file=d:/work/data/nowcoder/community.log
但是修改配置也不方便,可以在resources下添加logback-spring.xml ,会自动启用该配置
#账号配置
git config --list
git config --global user.name "myName"
git config --user..email "xxxxx"
#本地仓库
git init
git status -s
git add *
git commit -m '....'
#生成密钥
ssh-keygen -t rsa -C "邮箱"
#推送已有项目
git remote add origin
git push -u origin master
#克隆已有项目
git clone https://git.xxxxxxxxxxxxxxxxxx.git
pom中添加 spring-boot-starter-mail 依赖
邮箱参数配置
#MailProperties
spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=zhujinliang89@sina.com
spring.mail.password=6656762999867642
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
邮箱工具类
@Component
public class MailClient {
private static final Logger logger = LoggerFactory.getLogger(MailClient.class);
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;//服务器作为发送方是固定的
//收件人 + 主题 + 内容
public void sendMail(String to, String subject, String content) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("发送邮件失败" + e.getMessage());
}
}
}
html邮件模板
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>邮件示例</title>
</head>
<body>
<p>欢迎您,<span style="color: darkorchid;" th:text="${username}"></span>!</p>
</body>
</html>
在测试类中
@Autowired
private MailClient mailClient; //自己写的bean
@Autowired
private TemplateEngine templateEngine;//springboot中已管理了模板引擎,只需注入
@Test
public void testMail(){
mailClient.sendMail("574524709@qq.com","test","test mail");
}
@Test
public void testHtmlMail(){
Context context = new Context();//注意是thymeleaf包下的
context.setVariable("username","sunday");//待会传递给模板
String content = templateEngine.process("/mail/demo", context);//把模板地址,数据
System.out.println(content);
mailClient.sendMail("574524709@qq.com", "HTML",content);
}
根据请求功能拆解
点击注册:打开注册页面
注册:提交表单数据、服务端作验证、服务端发送邮箱
激活:点击邮件的链接,访问服务端的链接
登入模块新建Controller:
//新方法:获取注册页面--> 返回模板路径
@RequestMapping(path = "/register",method = RequestMethod.GET)
public String getRegisterPage(){
return "/site/register";
}
提交注册数据:
先导一个工具包 Comments Lang 用于处理字符串
给项目写个域名community.path.domain=http://localhost:8080
新建工具类 CommunityUtil
public class CommunityUtil {
//生成激活码,提供字符串
public static String generateUUID(){
return UUID.randomUUID().toString().replaceAll("-","");//不想要有横线
}
//MD5加密(只能加密,不能解密)
public static String md5(String key){
if(StringUtils.isBlank(key)){//先简单判断下不为空,采用了commons lang包
return null;
}else {
return DigestUtils.md5DigestAsHex(key.getBytes(StandardCharsets.UTF_8));//这是spring自带的加密方法
}
}
}
UserService
//注入
@Autowired
private UserMapper userMapper;
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
//注册时发送激活码需要带上域名和项目名,因此从properties中注入
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
@Autowired
private LoginTicketMapper loginTicketMapper;
//注册业务的方法:
//传入 User(来自Controller由前端封装的)
//返回 Map :用于返回信息,账号不为空,密码不为空等
/**
业务逻辑:
1.参数判断(空值)
2.账号是否已存在(userMapper.selectByName()获取user)
3.邮箱是否已注册(userMapper.selectByEmail()获取user)
4.注册用户,先要对密码加密
user.setSalt();随机生成salt
user.setPassword(原密码+salt然后MD5加密)
生成随机头像,user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png",new Random().nextInt(1000)));
记录创建事件user.setCreateTime(new Date());
5.发送激活邮件
//给用户发邮件,用于激活 模板 activation.html*/
//通过配置使得mybatis对insert自动生成id并回填
mybatis.configuration.useGeneratedKeys=true
///////////////////////////////////////////
Context context = new Context();
context.setVariable("email",user.getEmail());
//动态拼接用户能点的路径(每个用户的激活页面是不同的) :101 是用户id,code是激活码
//http://localhost:8080/community/activation/101/code
String url = domain+contextPath+"/activation/"+user.getId()+"/"+user.getActivationCode();
context.setVariable("url",url);
//生成模板引擎
String content = templateEngine.process("/mail/activation",context);
mailClient.sendMail(user.getEmail(), "账号激活",content);
Controller
工作:采用User对象接受 前端传入的表单数据
调用service方法 获取map
根据返回的map作判断,,然后将消息封装给model返回模板
修改html
<!--修改表单 提交方法post,action路径-->
<form class="mt-5" method="post" th:action="@{/register}"></form>
<!--注意添加name属性,要和实体类属性一样-->
<input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|" th:value="${user!=null?user.userName:''}"
id="username" name="username" placeholder="请输入您的账号!" required>
邮箱激活
//路径就是发给邮箱的链接
@RequestMapping(path = "/activation/{userId}/{code}",method = RequestMethod.GET)
//设计常量接口,激活成功,重复激活,激活失败
//激活方法:返回成功或失败
//参数:userId和code
//逻辑:查询用户,获取激活码,验证正确
User user = userMapper.selectById();
//通过user.getStatus()获取状态
http性质:无状态的
//测试:
@RequestMapping(path = "/cookie/set",method = RequestMethod.GET)
@ResponseBody
public String setCookie(HttpServletResponse httpServletResponse){
//需要用到response对象,在方法参数传入
Cookie cookie = new Cookie("code","123");//每个cookie只能有一对字符串
//需要指定路径
cookie.setPath("/community");
//浏览器得到cookie默认存内存中,所以关闭浏览器cookie会消失,需要设置生效时间
cookie.setMaxAge(60*10*60);//秒
httpServletResponse.addCookie(cookie);
return "set cookie";
//测试返回:
//如果要获取全部cookie,可以传入request对象,但是获得cookie数组,需要遍历获得,因此可使用注解获取指定cookie
//@CookieValue("")
@RequestMapping(path = "/cookie/get",method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code){
//就可以获取code 使用
return "get cookie";
}
Sesscion:MVC自动创建,只要注入就行
@RequestMapping(path = "/session/get",method = RequestMethod.GET)
@ResponseBody
public String setSession(HttpSession httpSession){
httpSession.setAttribute("id",1);
return "set session";
}
分布式部署:nginx实现负载均衡
- 粘性session:同一ip的请求均分配到指定一台服务器上
- 同步session:服务器将session同步给所有服务器
- 共享session:有一台单独的服务器用于处理session,其他服务器与该服务器
- 主流:不使用session,而是用cookie,部分不适合存cookie的存数据库里,数据库集群备份
- 更好的做法:不存在关系型数据库(硬盘)中,而是NOSQL中
@RequestMapping(value = "/kaptcha",method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response, HttpSession session){//由于传入的是图片因此返回值为void;验证码不能存在浏览器端,且需要跨请求,因此使用session
//先在上面把bean注入
String text = kaptchaProducer.createText();//根据配置会得到四位的字符串
BufferedImage image = kaptchaProducer.createImage(text);
//将验证码存入session
session.setAttribute("kaptcha",text);
//将图片输出给浏览器,注意使用response
response.setContentType("image/png");
try {
ServletOutputStream os = response.getOutputStream();//字节流
ImageIO.write(image,"png",os);
} catch (IOException e) {
//这里就不要捕获异常了,
logger.error("响应验证码失败",e.getMessage());
}//不需要关闭os,spring会统一处理
}
模板修改:点击刷新验证码来刷新,而不是整个页面刷新,需要js实现
<div class="col-sm-4">
<img th:src="@{kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/><!--这个id 是用于js-->
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
</div>
<script><!--基于jquery的-->
function refresh_kaptcha(){
var path = CONTEXT_PATH +"/kaptcha?p="+Math.random();//CONTEXT_PATH是我们写在js/global.js的全局变量,便于以后改动
//这里加上参数p=Math.random()是为了欺骗浏览器,不要让它觉得每次访问一样的路径而采用缓存
$("#kaptcha").attr("src",path);//将上文中id=kaptcha 的src属性改成path
}
</script>
如何检查登入退出状态;数据库表LoginTicket,存userId,status,Expired,核心是ticket
1.创建实体类
2。Dao层:
//登入:在表中插入数据
//查询:根据ticket
//退出:修改状态status
//@Insert({"","",""})//里面的多个字符串自动合成一条sql
//登入成功后要插入凭证//需要声明主键自动生成,@Options,且需要将生成的值注入给对象,keyProperty = "id"
@Insert({
"insert into login_ticket (user_id,ticket,status,expired) ",//加个空格断开
"values(#{userId},#{ticket},#{status},#{expired})"
})
@Options(useGeneratedKeys = true,keyProperty = "id")
int insertLoginTicket(LoginTicket loginTicket);
//查询方法:围绕ticket
@Select({
"select id,user_id,ticket,status,expired ",
"from login_ticket where ticket=#{ticket}"
})
LoginTicket selectByTicket(String ticket);
//修改凭证状态:不删除
@Update({
"update login_ticket set status=#{status} where ticket=#{ticket}"
})
int updateStatus(String ticket,int status);
//学习:假如需要动态sql时
/*@Update({
"<script>",
"update login_ticket set status=#{status} where ticket=#{ticket} ",
"<if test=\"ticket!=null\">", //这里的双引号用于转义
"and 1 =1",
"</if>",
"</script>"
})*/
实现登入功能,在UserService
//参数:来自Controller传递的username,password,以及expiredSecond
//返回map,封装消息
1.验证字段不为空;
2.数据库查询(账号不存在,账号未激活,);
3.获取salt,然后将明文密码加密,验证密码;
4.登入成功,生成登入凭证loginTicket,凭证需要发给客户端,类似于session
表现层,在LoginController
同样是login方法,但是参数不同,get获取页面,post为登入
方法参数:账号,密码,验证码,记住我,model,session,response
(session存之前的验证码字符串,response用于获取cookie)
返回:String
//取出验证码
String kaptcha = (String)session.getAttribute("kaptcha");
if(StringUtils.isBlank(kaptcha)||StringUtils.isBlank(code)||!kaptcha.equalsIgnoreCase(code)){
model.addAttribute("codeMsg","验证码不正确");
return "/site/login";
}
//检查账号密码 ,由service处理
//如果勾选了“记住我”,则存的时间长一点
//这里再次在CommunityConstant类添加常量
int expiredSecond = rememberme?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSecond);
//如果map总包含ticket,就是成功了
if(map.containsKey("ticket")){
Cookie cookie = new Cookie("ticket",map.get("ticket").toString());
cookie.setPath(contextPath);//表示整个项目下cookie都是有效的:注入properties中的值
cookie.setMaxAge(expiredSecond);
response.addCookie(cookie);//将cookie发给用户
return "redirect:/index";
}else {
model.addAttribute("usernameMsg",map.get("usernameMsg"));
model.addAttribute("passwordMsg",map.get("passwordMsg"));
return "/site/login";
}
如果是实体对象,MVC会将此装进model中,而一般变量(本方法用的String)则不会在model中,但是在request 对象中
这里的 ${param.password}
相当于 request.get(password)
;
<div class="col-sm-10">
<input type="password" th:class="|form-control ${passwordMsg!=null?'is-valid':''}|" name="password" th:value="${param.password}"
id="password" placeholder="请输入您的密码!" required>
<div class="invalid-feedback" th:text="${passwordMsg}">
密码长度不能小于8位!
</div>
</div>
退出:把凭证改为失效状态,返回首页
//userService
public void logout(String ticket){
loginTicketMapper.updateStatus(ticket,1);
}
//LoginController
@RequestMapping(path = "logout",method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket){
userService.logout(ticket);
return "redirect:/login";//默认get请求
}
拦截器: